타입 호환성 (Type Compatibility)

저뉼(스님?)·2022년 11월 6일
0

나만의 설명

목록 보기
1/11
post-custom-banner

다음의 글을 재밌게 읽어서 관련 내용을 이해하고 재서술해 보았다.
TypeScript 타입 시스템 뜯어보기: 타입 호환성, 김병묵, 토스 기술 블로그
특히, Branding 부분을 보강하고 좀 더 쉽게 풀었다.


structural subtyping

Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing.
Type Compatibility, 공식문서

The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x.
Starting out, Type Compatibility, 공식문서

structural subtyping? 멤버(구조)만 같으면 호환되는 것인데, 아래 코드를 보면 쉽게 이해가 될 것이다.

동물 타입을 만들고 동물의 표준체중대비백분율을 계산하는 함수를 만든다.

type 동물 = {
	height: number, // cm
	weight: number, // kg
};

function 표준체중대비백분율(animal: 동물): number {
    const 표준체중 = animal.height / 100 * animal.height / 100 * 22;
	return +(animal.weight / 표준체중 * 100).toFixed(1);
}

동물의 표준체중대비백분율을 계산하는 함수에 동물 타입이 아닌 object를 전달한다.

const yul = { // type이 `동물`이 아님
	weight: 40,
	height: 145,
	name: "율"
};
표준체중대비백분율(yul); // OK.

함수에 전달한 argument yul의 type이 동물이 아니더라도 weightheight 값을 갖고 있다면 호환된다.
이런 허용은 바로 다음에 알아 볼 nominal subtyping 대비 코드량을 줄여 준다.

nominal subtyping

만약 structural subtyping을 허용하지 않았다면?

type 사람 = 동물 & { // 동물 type과 호환됨을 명시
	name: string
};

const yul: 사람 = { // 사람 type임을 명시
	weight: 40,
	height: 145,
	name: "율"
};
표준체중대비백분율(yul);

C#이나 Java에서 하는 것처럼, 매번 위 코드처럼 명확하게 적어(nominal subtyping) 줘야 했을 것이다.

structural subtyping 예외

TypeScript takes the stance that there’s probably a bug in this code. Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error:
Excess Property Checks, 공식문서

언제나 허용되는 것은 아니고 예외가 있다. 함수에 전달된 argument가 따끈따끈한 object 그 자체('fresh' object literal type)인 경우이다.

표준체중대비백분율({
	weight: 40,
	height: 145,
	name: "율" // ERROR!
});

변수에 할당 후 변수를 전달했을 때는 괜찮았지만 바로 전달했을 때는 name 때문에 다음과 같은 에러가 발생한다.

Object literal may only specify known properties, and 'name' does not exist in type '동물'

왜 이 경우만 허용하지 않느냐? 허용해 줘 봐야 좋을 게 없다.
변수에 할당되지 않았으니 다른 함수에 사용하지도 못할 argument다. 그런데 굳이 함수가 필요로 하지도 않는 값을 끼워 넣을 수 있게 허락한다?
이점은 커녕, 표준체중대비백분율() 함수 호출 부분만 보면 함수가 name 값을 사용한다고 오해할 수 있다.

그런데 예외의 예외도 있다. (이 뭔 법학도 아니고..) 다시 말해, 우회할 수 있다.

Excess Property Checks 우회

Keep in mind that for simple code like above, you probably shouldn’t be trying to “get around” these checks.
Excess Property Checks, 공식문서

type assertion을 사용하거나 string index signature를 추가해서 우회할 수 있지만, 그렇게 하지 않는 것을 권장한다. 우회를 할 게 아니라 type을 고치는 게 맞는 경우가 대부분일 것이기 때문이라고 한다.

강하게 제약하기

반대로, 호환성을 거부하고 더 강하게, 정확히 일치하지 않는 한 금지하고 싶을 때도 있을 것이다.

섭씨와 화씨라는 다른 타입을 만들고 섭씨를 화씨로 변환하는 함수를 만들어 보자.

type Celsius = number; 
type Fahrenheit = number;

function convertToFahrenheit(degreeCelsius: Celsius): Fahrenheit {
    return degreeCelsius * 9 / 5 + 32 as Fahrenheit;
}

섭씨와 화씨로 타입을 구분했지만 다음의 코드를 보면 호환이 잘 돼서 타입 구분을 의미 없게 만든다.

// Celsius type
const degreeCelsius = 100 as Celsius;
convertToFahrenheit(degreeCelsius); // OK

// fresh Celsius type
convertToFahrenheit(100 as Celsius); // OK

// fresh number type
convertToFahrenheit(100); // OK..?

// Fahrenheit type
const degreeFahrenheit = 451 as Fahrenheit;
convertToFahrenheit(degreeFahrenheit); // OK..?

위 코드의 모든 함수 호출에 에러가 발생하지 않는다. 섭씨를 화씨로 변환하는 함수라서 섭씨 온도만 들어가야 할 텐데 멤버(구조)만 같으면 다 들어가서 문제다.
처음 두 호출 말고는 에러가 나야 하지 않겠는가?

Branding

Template literal type을 이용해서 호환성을 막을 수 있다.

type Celsius = number & {
    __brand: "Celsius" // string 값이 아닌 template literal type
};

type Fahrenheit = number & {
    __brand: "Fahrenheit" // string 값이 아닌 template literal type
};

function convertToFahrenheit(degreeCelsius: Celsius): Fahrenheit {
    return degreeCelsius * 9 / 5 + 32 as Fahrenheit;
}

위 코드에서 CelsiusFahrenheit를 branded type으로 정의했다. 그래도 base type은 여전히 number이기 때문에 함수 안에서 number처럼 사용한다.

// Celsius type
const degreeCelsius = 100 as Celsius;
convertToFahrenheit(degreeCelsius); // OK

// fresh Celsius type
convertToFahrenheit(100 as Celsius); // OK

// fresh number type
convertToFahrenheit(100); // ERROR!

// Fahrenheit type
const degreeFahrenheit = 451 as Fahrenheit;
convertToFahrenheit(degreeFahrenheit); // ERROR!

이제 실수로 섭씨 온도를 받는 함수에 다른 종류의 값을 전달할 일이 없어졌다.

참고

generic type을 이용하여 구현할 수도 있다. 동작은 같다.

type Brand<K, T> = K & {
	__brand: T
};
type Celsius = Brand<number, "Celsius">;
type Fahrenheit = Brand<number, "Fahrenheit">;

참고 2 (CelsiusFahrenheit의 기본 타입이 number가 아닌 object라면?)

type Celsius = {
	value: number,
};
type Fahrenheit = {
    value: number,
};

function convertToFahrenheit(degreeCelsius: Celsius): Fahrenheit {
    return { value: degreeCelsius.value * 9 / 5 + 32 } as Fahrenheit;
}
// Celsius type
// const degreeCelsius = { value: 100 } as Celsius;
const degreeCelsius: Celsius = { value: 100 };
convertToFahrenheit(degreeCelsius); // OK

// fresh Celsius type
convertToFahrenheit({ value: 100 } as Celsius); // OK

// object literal type
const objLiteralDegreeCelsius = { value: 100 };
convertToFahrenheit(objLiteralDegreeCelsius); // OK

// fresh object literal type
convertToFahrenheit({ value: 100 }); // OK

// Fahrenheit type
const degreeFahrenheit = { value: 451 } as Fahrenheit;
convertToFahrenheit(degreeFahrenheit); // OK

// fresh Fahrenheit type
convertToFahrenheit({ value: 451 } as Fahrenheit); // OK
type Celsius = {
    value: number,
} & { __brand: "Cel" };

type Fahrenheit = {
    value: number,
} & { __brand: "Fah" };
// Celsius type
const degreeCelsius = { value: 100 } as Celsius;
convertToFahrenheit(degreeCelsius); // OK

// fresh Celsius type
convertToFahrenheit({ value: 100 } as Celsius); // OK

// object literal type
const objLiteralDegreeCelsius = { value: 100 };
convertToFahrenheit(objLiteralDegreeCelsius); // ERROR!

// fresh object literal type
convertToFahrenheit({ value: 100 }); // ERROR!

// Fahrenheit type
const degreeFahrenheit = { value: 451 } as Fahrenheit;
convertToFahrenheit(degreeFahrenheit); // ERROR!

// fresh Fahrenheit type
convertToFahrenheit({ value: 451 } as Fahrenheit); // ERROR!
type Brand<K, T> = K & { __brand: T };

type Celsius = Brand<{
    value: number,
}, "Cel">;

type Fahrenheit = Brand<{
    value: number,
}, "Fah">;
post-custom-banner

0개의 댓글