런타임에 모든 변수는 Javascript값으로부터 정해지는 각자의 고유한 값을 가진다.
그러나 코드가 실행되기 전, 즉 타입스크립트가 오류를 체크하는 순간에는 타입을 가지고 있다. 할당 가능한 값들의 집합이 타입이라고 생각하면 된다.
이 집합은 타입의 범위라고 부르기도 한다.
null과 undefined는 strictNullChecks 여부에 따라 number일수도 아닐 수도 있다.
(strictNullChecks가 false인 경우 null과 undefined는 어디에도 속할 수 있다.)
가장 작은 집합은 아무 값도 포함하지 않는 공집합이며 타입스크립트에서는 never 타입이다.
never 타입으로 선언된 변수의 범위는 공집합이기 때문에 아무런 값도 할당할 수 없다.
하여 보통 도달하면 안되는곳에 할당한다.(ex. 에러객체)
리터럴 타입은 유닛 타입이라고도 불리며 한 가지 값만을 포함하는 타입이다.
type A = 'A';
type twelve = 12;
위와 같은 경우는 'A'라는 문자 자체가 하나의 type이 된다.
따라서 아래와 같이 변수를 선언하게 되면 상수 A는 변하는 값이 아니기에 문자 'A'는 그대로 type으로 적용된다.
const A = 'A';
두개 혹은 세개로 묶으려면 유니온(union) 타입을 사용한다.
type AB = 'A' | 'B'
type AB12 = 'A' | 'B' | 12
세개 이상의 타입을 묶을 때도 동일하게 | 로 이어주면 된다.
유니온 타입은 값 집합들의 합집합을 일컫는다.
다양한 타입스크립트 오류에서 '할당 가능한' 이라는 문구를 볼 수 있다.
이 문구는 집합의 관점에서 '~의 원소(값과 타입의 관계)' 또는 '~의 부분 집합(두 타입의 관계)'을 의미한다.
const a: AB = 'A' // 정상, 'A'는 집합 {'A', 'B'}의 원소입니다.
const c: AB = 'C'
// ~"'C'" 형식은 'AB' 형식에 할당할 수 없습니다.
"C"는 유닛타입 이다. 범위는 단일 값 "C"로 구성되며 "AB"의 부분 집합이 아니므로 오류이다.
집합의 관점에서, 타입 체커의 주요 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이라고 볼 수 있다.
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death: Date;
}
type PersonSpan = Person & Lifespan
&연산자는 두 타입의 인터섹션(intersection: 교집합)을 계산한다.
언뜻 보기에 Person과 Lifespan 인터페이스는 공통으로 가지는 속성이 없기 때문에, PersonSpan 타입을 공집합(never)으로 예상할 수 있으나 그렇지 않다.
타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용된다.
즉, Person과 Lifespan을 모두 가지는 값에 대한 type을 지칭하게 된다.
Person | LifeSpan 과 Person & LifeSpan과의 차이는 &는 2개의 type을 모두 만족시켜야 하고 |는 둘 중 하나만 만족시켜도 된다.
{}안에 birth만 있어도 되고, name만 있어도 되고, birth와 name 둘 다 있어도 된다.
하지만 death만 있는것은 안된다.
왜냐하면 Person과 LifeSpan 둘 중 어느것도 만족시키지 못하기 때문이다.
조금 더 일반적으로 PersonSpan 타입을 선언하는 방법은 extends 키워드를 쓰는것이다.
interface Person {
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death?: Date;
}
타입이 집합이라는 관점에서 extends의 의미는 '~의 부분 집합'이라는 의미로 받아들일 수 있다.
PersonSpan 타입의 모든 값은 문자열 name 속성을 가져야 한다.
그리고 birth 속성을 가져야 제대로 된 부분 집합이 된다.
처음엔 어째서 PersonSpan이 Person의 부분집합인지 이해하지 못했다.
PersonSpan이 Person보다 더 많은 값을 가지고 있는데 왜?
현재까지 이해하기로는 타입 연산은 인터페이스의 속성이 아닌 값의 집합(타입의 범위)에 적용되므로 Person은 name만 가지고 있어도 성립하지만 PersonSpan은 name과 birth를 모두 가지고 있어야 성립하기 때문에 값의 범위로 본다면 더욱 많은 조건이 성립되어야만 하므로 Person 벤다이어그램 내부에 있는 하위 집합이라고 생각하기로 하였다.
extends가 ~의 부분집합이라는 것을 상기하며 아래의 예제를 보자.
function getKey<K extends string>(val:any, key:K){
...
}
위의 에제에서 generic K는 string의 extends이므로(string을 상속하므로) string 타입의 부분집합이어야 한다.
getKey({}, 'x'); //정상 'x'는 string을 상속
getKey({}, Math.random() < 0.5 ? 'a' : 'b'); //정상 'a'|'b'는 string을 상속
getKey({}, document.title); //정상 'string은 string을 상속
getKey({}, 12); // ~~ 12 형식의 인수는 string 형식의 매개변수에 할당될 수 없습니다.
마지막 12를 제외한 모든 예시들은 결국 모두 string의 부분집합이 될 수 있지만 12는 number로 string의 부분집합이 될 수 없기에 error를 반환한다.
Exclude<T, U> = T extends U ? never : T
사전적 정의는 "T가 U에 속하면 never를 내보내고 아니면 T를 내보낸다" 이다.
어려운 말이다.
흔히 쉽게 이해할 수 있는 방법은 T에서 U에 해당하는걸 제외하라는 의미로 받아들이면 된다.
ex)
Exclude<number, number> === never
Exclude<string|Date, string|number> === Date
타입들이 엄격한 상속 관계가 아닐 때는 집합 스타일이 더욱 바람직하다.
예를들어, string|number 와 string|Date 사이의 인터섹션(교집합)은 공집합이 아니며(string 이다) 서로의 부분집합도 아니다.
이 타입들은 엄격한 상속관계가 아니더라도 범위에 대한 관계는 명확하다.
유니온 타입이 상속 관점에서는 어색하지만, 집합 관점에서는 자연스럽다.
타입이 집합이라는 관점은 배열과 튜플의 관계 역시 명확하게 만든다.
const list = [1, 2]; // 타입은 number[]
const tuple: [number, number] = list;
// ~~ number[] 타입은 [number, number] 타입의 0,1 속성에 없습니다.
number[]는 [number, number]의 부분집합이 아니기 때문에 할당할 수 없다.
number[]는 개수제한이 없고 [number, number]는 배열안의 number의 개수가 2개로 고정되어있기 때문이다.