: 프로그래밍 언어에서 변수가 어떤 종류의 값(데이터)를 가질 수 있는지 정의하고 검증하는 규칙
명목적 타입 시스템 : 두 타입의 이름이 같아야 같은 타입으로 간주한다.
ex) java는 클래스명이 동일해야 같은 타입으로 인식됨
구조적 타입 시스템 : 두 타입의 구조가 같으면 같은 타입으로 간주한다.
⇒ 객체의 속성과 메서드가 동일하면 같은 타입으로 인식됨
typescript
는? 구조적 타입시스템을 사용한다
// 익명함수
function() {
return "anonymous function";
}
// 객체 리터럴
const obj_literal = {
name: 'jane',
age: 25
}
: typescript의 타입 호환성은 구조적 서브타이핑(strunctural subtyping)을 기반으로 한다.
type Food = {
protein: number;
carbon: number;
fat: number;
}
// parameter 타입을 Food로 지정해준다
function calculateCalories(food: Food) {
return food.protein * 4 + food.carbon * 4 + food.fat * 9;
}
const food1:Food = {
protein: 10,
carbon: 10,
fat: 10
}
calculateCalories(food1); // 170
/**
* 구조적 서브타이핑 : 상속관계 명시X. 객체의 프로퍼티 기반으로 타입 호환
*/
const food2 = {
protein: 15,
carbon: 15,
fat: 15
}
calculateCalories(food2); // 255
/**
* 명시적 서브타이핑 : 상속관계 명시O
*/
// Food 타입을 상속받은 Burger 타입을 정의하고
type Burger = Food & {
brandName: string;
}
const burger1:Burger = {
protein: 20,
carbon: 20,
fat: 20,
brandName: 'mc'
}
// 함수의 인자로 전달해도 문제가 발생하지 않는다 => 타입호환성 때문
calculateCalories(burger1); // 340
Fresh literal은 타입이 호환되지 않는다.
// 함수 인자로 literal 객체를 직접 전달하면 ?? => 에러 발생
calculateCalories({
protein: 30,
carbon: 30,
fat: 30,
brandName: 'burger king'
});
// ERROR : Object literal may only specify known properties,
// and 'brandName' does not exist in type 'Food'.
Fresh literal 를 알아보기 전에..
typescript는 어떻게 타입을 검사할까?
typescript 컴파일러의 동작 과정을 간단히 정리해보면,
1) 소스코드를 AST(Abstract Syntax Tree)로 변환한 뒤 (parser.ts, scanner.ts)
2) 타입 검사를 수행하고 (binder.js, checker.ts)
3) javascript 소스코드로 변환한다. (emitter.js, transformer.ts)
이때 구조적 서브타이핑
과 타입 호환
에 관한 부분은 타입 검사와 연관되어있는
checker.js의 hasExcessProperties()
함수에서 처리한다
// checker.js
// 1) getObjectFlags로 객체 타입 플래그 체크
// -> ObjectFlags.FreshLiteral 일 경우
const isPerformingExcessPropertyChecks =
getObjectFlags(source) & ObjectFlags.FreshLiteral;
// 2) FreshLiteral에 대한 excess properties 존재 여부를 체크한다.
if (isPerformingExcessPropertyChecks) {
if (hasExcessProperties(source as FreshObjectLiteralType)) {
// excess properties가 존재할 경우 error
reportError();
}
}
// FreshLiteral이 아닌 경우 위 if 분기를 skip. 타입 호환을 허용
.
.
.
Fresh Literal 이란 무엇일까
모든 object literal
은 초기에 fresh
하다고 간주되며
1) 타입 단언(type assertion)을 하거나,
2) 타입 추론에 의해 object literal의 타입이 확장되면 freshness가 사라진다.
⇒ object literal을 변수에 할당하는 경우, 이 두 가지 중 하나가 발생되어 freshness가 사라진다.
위에서는 함수에 object literal을 인자로 바로 전달했기 때문에 freshness가 사라지지 않았고, 타입이 호환되지 않아 오류가 발생했다.
그렇다면 왜 Fresh literal은 타입 호환을 허용하지 않을까?
⇒ 코드 파악을 어렵게 하고, 오류를 숨기는 등 여러 부작용을 유발할 수 있다.
다른 개발자가 excess properties를 함수 연산에 필수적인 값으로 오해할 수 있다.
( calculateCalories에서는 excess properties로 전달한 brandName이 사용되지 않는데 필수값으로 오해할 수 있음 )
excess properties 이름에 오타가 있더라고 발견되지 않는다.
( type Burger 인 경우에만 호환을 허용하고 싶은데 Burger 에 없는 brandNeam 프로퍼티를 전달한 경우에도 허용한다면 문제가 된다 )
// 부작용 1
const calorie1 = calculateCalories({
protein: 29,
carbon: 48,
fat: 13,
burgerBrand: 'burger king' // ???? calculateCalorie에 필요한지 알 수 없음
});
// 부작용 2
const calorie2 = calculateCalories({
protein: 29,
carbon: 48,
fat: 13,
brandNeam: 'wrong name' // ???? property 이름에 오타가 있는지 확인할 수 없음
});
Fresh object에 대해서는 타입 호환성을 제공해서 얻는 이점보다는 부작용이 더 많아
타입 호환성을 지원하지 않는다.
그럼에도 fresh object에 대한 타입 호환성을 허용하고자 한다면
index signature
를 사용하거나 tsconfig 에 suppressExcessPropertyErrors=true
로 설정하면 된다.
// index signature 를 사용하는 방법
type Food = {
protein: number;
carbon: number;
fat: number;
[key: string]: any; // excess property 를 추가할 수 있게 한다
}
반대로 타입 호환성을 엄격하게 제한하고 싶다면 브랜드 타입을 사용하면 된다.
type Brand<K, T> = K & {__brand: T};
type Food = Brand<{
protein: number;
carbon: number;
fat: number;
}, 'Food'>
const burger1 = {
protein: 20,
carbon: 20,
fat: 20,
brandName: 'mc'
}
calculateCalories(burger1); // ERROR : Argument of type
// '{ protein: number; carbon: number; fat: number; brandName: string; }'
// is not assignable to parameter of type 'Food'.