2장에서는 타입 시스템의 기초부터 살펴본다.
타입스크립트를 설치하면 다음 두가지를 실행할 수 있다.
tsc
)tsserver
)보통 타입스크립트 컴파일러를 실행하는 것이 주된 목적이지만, 타입스크립트 서버 또한 '언어 서비스'를 제공한다는 점에서 중요하다. 언어서비스에는 코드 자동 완성, 명세(사양, specification) 검사, 검색, 리팩터링이 포함된다.
타입스크립트가 값을 추론할 때 기대한 값과 추론한 타입이 다를 수 있는데, 이럴 땐 직접 타입을 명시한다.
이외에도 편집기 상의 타입 오류를 살펴보는 것도 타입 시스템의 성향을 파악하는데 좋은 방법이다. 아래의 코드를 예시로 살펴본다.
function getElement(elOrId: string|HTMLElement|null): HTMLElement {
if (typeof elOrId === 'object') {
return elOrId;
// ~~~~~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'
} else if (elOrId === null) {
return document.body;
} else {
const el = document.getElementById(elOrId);
return el;
// ~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'
}
}
자바스크립트에서 typeof null
은 object
이므로, elOrId
는 여전히 분기문 내에서 null
일 가능성이 있다. 그러므로 의도한 대로 분기처리를 하려면 처음에 null
체크를 추가해서 바로 잡는다.
언어 서비스는 라이브러리와 라이브러리의 타입 선언을 탐색할 때도 도움이 된다. 코드 내에서 fetch
함수를 호출 하고 이 함수를 더 알아보길 원한다고 할 때, 편집기는 Go to Definition(정의로 이동)
옵션을 제공한다.
이 옵션을 선택하면 타입스크립트에 포함되어 있는 DOM 타입 선언인 lib.dom.d.ts
로 이동한다. 이 파일에서 더 많은 타입을 탐색하다 보면, 어떻게 라이브러리가 모델링되었는지, 어떻게 오류를 찾아낼지 살펴볼 수 있다.
가장 작은 집합은 아무 값도 포함하지 않는 공집합이며, 타입스크립트에서는 never
타입이 여기에 해당한다. never
타입으로 선언된 변수의 범위는 공집합이기 때문에 아무런 값도 할당할 수 없다.
다음으로 작은 집합은 한가지 값만 포함하는 타입이다. 이들은 타입스크립트에서 유닛(unit)타입이라고 불리는 리터럴(literal) 타입이다.
type A = 'A';
type B = 'B';
type Twelve = 12;
타입스크립트의 타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용된다.
interface Identified {
id: string;
}
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
}; // OK
&
연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산한다. 값의 집합으로 생각해보면, 인터섹션 타입의 값은 각 타입 내 속성을 모두 포함하는 것이 일반적인 규칙이다.
규칙이 속성에 대한 인터섹션에 관해서는 맞지만, 유니온에서는 그렇지 않다.
interface Identified {
id: string;
}
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
type K = keyof (Person | Lifespan); // Type is never
앞의 유니온 타입에 속하는 것은 어떠한 키도 없기 때문에, 유니온에 대한 keyof
는 공집합(never
)이어야 한다.
'A는 B를 상속(extends)', 'A는 B에 할당 가능', 'A는 B의 서브타입'은 'A는 B의 부분 집합'과 같은 의미다.
타입스크립트의 심벌(symbol)은 타입 공간이나 값 공간 중의 한 곳에 존재한다.
interface Cylinder {
radius: number;
height: number;
}
const Cylinder = (radius: number, height: number) => ({radius, height});
심벌은 이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있기 때문에 혼란스러울 수 있다. 상황에 따라서 Cylinder
는 타입으로 쓰일 수 있고, 값으로도 쓰일 수 있어서 오류를 야기하기도 한다.
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape.radius
// ~~~~~~ Property 'radius' does not exist on type '{}'
}
}
instanceof
는 자바스크립트의 런타임 연산자이고, 그러므로 타입이 아닌 값에 대해서 연산을 하기 때문에, instanceof Cylinder
는 타입이 아니라 함수를 참조해서 에러가 발생한다.
두 공간에 대한 개념을 잡으려면 타입스크립트 플레이그라운드를 활용하는 것이 좋다. 타입스크립트 소스로부터 변환된 자바스크립트 결과물을 보여준다. 컴파일 과정에서 타입 정보는 제거되기 때문에, 심벌이 사라진다면 그것은 타입에 해당될 것이다.
interface Person {
first: string;
last: string;
}
const p: Person = { first: 'Jane', last: 'Jacobs' };
// - --------------------------------- Values
// ------ Type
function email(p: Person, subject: string, body: string): Response {
// ----- - ------- ---- Values
// ------ ------ ------ -------- Types
// COMPRESS
return new Response();
// END
}
class Cylinder {
radius=1;
height=1;
}
function calculateVolume(shape: unknown) {
if (shape instanceof Cylinder) {
shape // OK, type is Cylinder
shape.radius // OK, type is number
}
}
type T1 = typeof p; // Type is Person
type T2 = typeof email;
// Type is (p: Person, subject: string, body: string) => Response
const v1 = typeof p; // Value is "object"
const v2 = typeof email; // Value is "function"
타입의 관점에서 typeof
는 값을 읽어서 타입스크립트 타입을 반환하지만, 값의 관점에서 typeof
는 자바스크립트 런타임의 typeof
연산자가 된다.
class
키워드는 값과 타입 두가지로 모두 사용되는데, 클래스에 대한 typeof
는 상황에 따라 다르게 동작한다.
const v = typeof Cylinder; // Value is "function"
type T = typeof Cylinder; // Type is typeof Cylinder
declare let fn: T;
const c = new fn(); // Type is Cylinder
속성접근자인 []
는 타입으로 쓰일 때에도 동일하게 동작한다. 그러나 obj['field']
와 obj.field
는 값이 동일하더라도 타입은 다를 수 있다. 따라서 타입의 속성을 얻을 때에는 반드시 첫번째 방법(obj['field']
)를 사용해야 한다.
const first: Person['first'] = p['first']; // Or p.first
// ----- ---------- Values
// ------ ------- Types
이와 같이 두 공간사이에서 다른 의미를 가지는 코드 패턴들이 있다.
패턴 | 타입 | 값 |
---|---|---|
this | 다형성(polymorphic) this | 자바스크립트의 this |
ㄴ 서브클래스의 메서드 체인을 구현할 때 유용 | ||
& 과 pipe | 인터섹션, 유니온 | AND 와 OR 비트 연산 |
const | as const 는 리터럴 또는 리터럴 표현식의 추론된 타입 | 새 변수 선언 |
extends | 서브 타입 또는 제너릭 타입의 한정자를 정의할 수 있음 | 서브 클래스 한정자 정의 |
ㄴinterface A extends B , Generic<T extends number> | ㄴclass A extneds B | |
in | 매핑된(mapped)타입에 등장 | for (key in object) |
자바스크립트에서는 객체 내의 각 속성을 로컬 변수로 만들어주는 구조 분해(descructuring) 할당을 사용할 수 있다. 그런데, 타입스크립트에서 구조 분해 할당을 하면 이상한 오류가 발생한다.
interface Person {
first: string;
last: string;
}
function email({
person: Person,
// ~~~~~~ Binding element 'Person' implicitly has an 'any' type
subject: string,
// ~~~~~~ Duplicate identifier 'string'
// Binding element 'string' implicitly has an 'any' type
body: string}
// ~~~~~~ Duplicate identifier 'string'
// Binding element 'string' implicitly has an 'any' type
) { /* ... */ }
값의 관점에서 Person
과 string
이 해석되었기 때문에 발생한 오류이다. 문제를 해결하려면 다음과 같이 타입과 값을 구분해야 한다.
function email(
{person, subject, body}: {person: Person, subject: string, body: string}
) {
// ...
}
타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 방법은 두가지가 있다.
interface Person { name: string };
const alice: Person = { name: 'Alice' }; // Type is Person
const bob = { name: 'Bob' } as Person; // Type is Person
as Persion
으로 표기한 형태가 타입 단언이다. 이렇게 타입을 지정하면 타입스크립트가 추론한 타입이 있더라도 Person
으로 간주한다.
이 방법 외에 !
를 사용해서 null
이 아님을 단언하는 경우도 있다.
const elNull = document.getElementById('foo'); // Type is HTMLElement | null
const el = document.getElementById('foo')!; // Type is HTMLElement
단언문은 컴파일 과정 중에 제거되므로, 타입 체커는 알지 못하지만 그 값이 null
이 아니라고 확신할 수 있을 때 사용해야 한다. 그렇지 않다면 null
인 경우를 체크하는 조건문을 사용해야 한다.
타입 단언문을 사용할 때는 타입스크립트보다 타입 정보를 더 잘알고 있는 상황에서 사용하는 것이 좋다.
자바스크립트의 기본형 값들에 대한 일곱 가지 타입(string
, number
, boolean
, null
, undefined
, symbol
, bigint
)와 객체 래퍼(String
, Number
, Boolean
, Symbol
, BigInt
)가 항상 동일하게 동작하는 것은 아니다. 예를 들어, String
객체는 오직 자기 자신하고만 동일하다.
타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링한다.
function isGreeting(phrase: String) {
return [
'hello',
'good day'
].includes(phrase);
// ~~~~~~
// Argument of type 'String' is not assignable to parameter
// of type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object;
// prefer using 'string' when possible
}
타입스크립트를 제대로 파고 들면 정말 공부할 게 많은 것 같아요.