타입스크립트를 설치하면 다음 두 가지를 사용할 수 있다.
이 두가지를 실행할 수 있으므로, 타입스크립트도 언어 서비스
를 제공한다고 할 수 있다.
언어 서비스는 코드 자동 완성, 명세, 검사, 검색, 리팩터링이 포함된다.
타입스크립트 편집기를 사용하면서 가장 좋은 점은, 심벌 위에 마우스 커서를 대면 타입스크립트가 타입을 어떻게 판단하는지 실시간으로 확인이 가능하다
이 때 타입이 기대되는 것과 다르다면 직접 값을 반환하는 것을 지정할 수 있다.
이러한 값의 추론 정보는 디버깅에 굉장히 유용하게 사용될 수 있다.
이런 에러들도 잡아낼 수 있다.
function getElement(elOrId: string|HTMLElement|null): HTMLElement {
if(typeof elOrId === "object") {
return elOrId;
}
// 'HTMLElemnt | null 형식은 'HTMLElement' 형식에 할당할 수 없습니다
첫번째 분기문의 의도는 HTMLElement
라는 객체를 골라내는 것이 목적이다. 그런데, elOrId
가 분기문 안에서는 null
이 될 수 있고 널 형식은 object
이므로 분기문이 에러가 생깁니다. 그래서 Null
을 확인할 수 있는 장치를 마련해둬야 합니다.
마지막으로 타입스크립트가 어떻게 모델링을 하고 있는지, 편집기의 Go to Definition
기능을 이용하면 쉽게 모델링된 모습을 확인하여, 타입스크립트가 어떻게 동작하는지를 쉽게 확인할 수 있다.
타입은 할당 가능한 값들의 집합
이라고 생각해야 한다. 이러한 집합은 타입의 범위라고 부르기도 하는데, 범위에 대해서 타입을 생각해보자.
가장 먼저, 제일 작은 집합은 아무것도 포함하지 않는 공집합이다. 이는 타입스크립트에서 never
타입으로 사용되며, 이로 선언된 변수의 범위는 공집합의 타입을 가지기 때문에 아무런 값도 할당할 수 없다.
다음으로 작은 집합은 한 가지 값만 포함하는 타입이다. 이는 유닛
타입이라고 불리는 리터럴타입이다.
type A = 'A';
type B = 'B';
type Twelve = 12;
이러한 타입을 두개 혹은 세개로 묶으려면 유니온타입을 사용한다.
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
세 개 이상의 타입을 묶을 때에도 동일하게 |
을 사용하여 이어주면 된다. 이는 집합들의 합집합을 의미한다.
const a: AB = 'A'; //정상 'A'는 집합 {'A', 'B'}의 원소이다.
const c: AB = 'C';
//~ '"C"' 형식은 'AB' 형식에 할당할 수 없습니다.
이렇게 타입스크립트의 에러 체크 과정에서 할당 가능한이라는 문구와 함께 안내를 하는 것을 볼 수 있다. 이는 집합의 관점에서, '~의 원소(값과 타입과의 관계)' 또는 '~의 부분집합(두 타입의 관계)'을 의미하는 것이다.
집합의 관점에서 위의 에러는 C
유닛 타입이 다른 집합의 부분 집합에도 속하지 않으므로 에러가 발생하는 것이다.
interface Person {
name: string;
}
interface LifeSpan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
&
연산자는 두 타입의 인터섹션을(교집합)을 계산한다. PersonSpan
이 공집합 타입으로 예상하기 쉬운데, 타입 연산자는 인터페이스의 속성이 아니라 값의 집합에 적용된다. 그렇기 때문에, 결과적으로 PersonSpan타입은 아래와 같은 결과를 가진다.
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23');
death: new Date('1954/06/07');
당연히, 구조적 타이핑의 원리에 따라서 위의 세 가지 속성보다 더 많은 속성을 가지는 값도 PersonSpan속성에 속할 수 있다.
type K = keyo (Person | Lifespan);
유니온 타입에 속하는 값은 어떠한 키도 없기 때문에, 유니온에 대한 keyof는 공집합이다.
정리하자면 아래와 같이 정리 가능하다.
keyof(A&B) = (keyof A) | (keyof B)
keyof(A|B) = (keyof B) & (keyof A)
인터페이스에 관한 것과 값에 대한 연산이 조금 다르게 작동하는 것을 헷갈려하면 안되겠다.
집합 간의 상속 관계를 표현하기 위하여 일반적으로 extend 키워드를 사용한다.
interface Person {
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death?: Date;
}
extends
의 의미는 ~에 할당 가능한 과 비슷한 의미를 가지고 있으며, '~의 부분 집합' 이라는 의미로 받아들일 수 있다. 이를 밴 다이어그램으로 표현하면, PersonSpan
은 Person
의 서브타입이다.
function getKey<K extends string>(val: any, key: K) {
//...
}
이 코드에서 K
는 string
의 부분 집합 범위를 가지는 어떠한 타입이 된다.
getKey({}, 'x');
getKey({}, Math.random() < 0.5 ? 'a' : 'b');
getKey({}, document.title);
getKey({}, 12);
// ~~ '12' 형식의 인수는 'string' 형식의 매개변수에 할당될 수 없습니다.
할당될 수 없습니다의 의미는, 상속할 수 없습니다라고 바꿀 수 있고, 상속의 범위에서 보면 마지막 줄에서 왜 오류가 발생하는지 쉽게 판단할 수 있습니다.
interface Point {
x: number;
y: number;
}
type PointKeys = keyof Point;
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] }
//..
}
const pts: Point[] = [{x: 1, y: 1}, {x: 2, y: 0}];
sortBy(pts, 'x'); //정상 'x'는 'x'|'y'를 상속
sortBy(pts, 'y'); //정상 'y'는 'x'|'y'를 상속
sortBy(pts, Math.random() < 0.5 ? 'x' : 'y' );
sortBy(pts, 'z');
// ~~ '"z"' 형식의 인수는 '"x" | "y"' 형식의 매개변수에 할당될 수 없습니다.
타입들이 엄격한 상속 관계가 아닐 때는 집합 스타일로 이들을 바라보는 것이 더욱 유용하다.
string|number
와 string | Date
사이의 인터섹션은 공집합이 아닌, string
이다. 타입이 집합이라는 관점은 배열과 튜플의 관계도 매우 명확하게 만든다.
이것 때문에 머리 아팠던 적이 한두번이 아닌데, 덕분에 명쾌하게 알 수 있었다.
const list = [1, 2]; //타입은 number[]
const tuple: [number, number] = list;
// ~~ 'number[]' 타입은 '[number, number]' 타입의 0, 1 속성에 없습니다.
이 친구가 성립하지 않는 반례를 들자면, 숫자 쌍인 [number, number]
은 빈 리스트 []
와 [1]
과 같은 반례가 존재하고 이를 포함하지 못한다. 그래서 number[]
는 [number, number]
의 부분 집합이 아니므로, 할당할 수 없는 것이다.
그러나, 반대로 할당하면 부분집합에 속하기 때문에 제대로 동작한다.
const triple: [number, number, number] = [1, 2, 3];
const double: [nubmer, number] = triple;
//'[number, number, number]'형식은 '[number, number]' 형식에 할당할 수 없습니다. 'length' 속성의 형식이 호환되지 않습니다.
구조적 타이핑의 관점에서 생각하면 위의 코드는 에러 없이 잘 수행되어야 한다.
타입스크립트는 숫자의 쌍을 {0: number, 1: number}
이렇게 모델링하는 것이 아니라, {0: number, 1: number, length: 2}
와 같이 모델링을 진행한다. 그래서 length
의 값이 맞지 않아서 할당문에 오류가 발생하는 것이다.
결론적으로 이번 아이템이 전달하고자 하는 바를 정리하면 아래와 같다
'A는 B를 상속', 'A는 B에 할당 가능', 'A는 B의 서브타입' 은 'A는 B의 부분 집합'과 같은 의미이다.
타입스크립트의 심벌(symbol)은 타입 공간이나 값 공간 중의 한 곳에 존재한다.
그래서 이 둘의 위치를 혼동하게 되면 혼란을 초래할 수 있으므로 이 둘을 잘 구분하여 사용할 수 있어야 한다.
interface Cylinder {
radius: number;
height: number;
}
const Cylinder = (radius: number, height: number) => ({radius, height});
interface
Cylinder는 타입으로 사용된다. const Cylinder
는 값으로 쓰인다.
즉 정리하자면 Cylinder는 값으로도 쓰일 수 있고, 타입으로 쓰일 수도 있다는 것이다.
이런 점이 오류를 발생시킬 수 있다.
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape.radius
// ~~ '{}'형식에 'radius' 속성이 없습니다.
}
}
instanceof
는 자바스크립트의 런타임 연산자이고, 값에 대하여 연산을 진행하기 때문에 Cylinder가 값으로 (함수로) 참조되어 이런 에러가 발생하는 것이다.
심벌이 타입인지 값인지는 한눈에 봐서 잘 알 수 없으므로, 문맥을 살펴야 한다.
일반적으로, type
이나 interface
다음에 오는 것은 타입, const
나 let
선언에 쓰이는 것은 값으로 평가할 수 있다.
이 부분이 헷갈린다면 타입스크립트 플레이그라운드를 사용하여 타입 심볼을 지울 수 있다. 이 사이트는 자바스크립트 표현으로 바꿔주기 때문이다.
interface Person {
first: string;
last: string;
}
const p: Person = {first: 'Jane', last: 'Jacos' };
이렇게 함수와 타입이 번갈아 나올 수 있다.
class
와 enum
은 타입과 값 두가지가 모두 가능한 예약어이다.
class Cylinder {
radius= 1;
height= 1;
}
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape //Cylinder 타입으로 판단됨.
shape.radius
}
}
클래스가 타입으로 쓰일 때는 형태(속성, 메서드)가 사용되는 반면, 값으로 쓰일 때는 생성자가 사용된다. 이 예시에서는 Cylinder가 타입으로 사용된 것이다.
연산자 중에서도 타입에서 쓰일 때와 값으로 쓰일 때 다른 역할을 하는 기능이 있다.
대표적으로 typeof
는 타입의 관점에서 값을 읽어 타입스크립트 타입을 반환한다. 값의 관점으로는 자바스크립트 런타임의 Typeof 연산자로 작동한다. 이렇게 상황에 따라 다르게 동작할 수 있다.
const v = typeof Cylinder; //값이 "function"
type T = typeof Cylinder; //타입이 "typeof Cylinder"
속성 접근자인 []는 타입을 쓰일 때 동일하게 동작한다.
obj['field']
와 obj.field
는 값은 동일해 보일 수 있어도, 타입의 속성을 얻을 때는 반드시 obj['field']를 사용해야 한다.
타입스크립트 코드가 잘 동작하지 않는다면, 타입 공간과 값 공간을 혼동해서 잘못 작성하였을 가능성이 크다. 구조 분해 할당을 이용하여 아래 코드를 바꾸어봤다고 하자.
function email (options: {person: Person, subject: string, body: string}) { //...}
을 바꾸어서
function email({person: Person, subject: string, body: string}) }
//...
}
타입스크립트에서 구조 분해 할당을 하면 위의 코드는 아래와 같은 에러를 반환한다.
바인딩 요소 'Person'에 암시적으로 'any' 형식이 있습니다.
'string' 식별자가 중복되었습니다.
바인딩 요소 'string' 에 암시적으로 'any'형식이 있습니다.
값의 관점에서 Person과 string이 해석되었기 때문에, 오류가 발생한다.
Person이라는 변수명과, string이라는 이름의 변수를 생성하였기 때문이다.
문제를 해결하기 위해서는 타입과 값을 구분하여 적어야 한다.
function email ({person, subject, body}: {person: Person, subject: string, body: string}) {
//...
}
매개변수에 대한 타입을 정확하게 지정해주어야 에러가 발생하지 않는다.