언어 변환 기능을 수행하는 Core Typescript Complier를 기반으로 작동하며, 파서, 바인더, 타입 체커, 에미터, 전처리기 로 구성되어 있다.
AST
(Abstract Syntax Tree) 를 생성한다. 이때 AST
의 경우 구문 분석으로 생성된 파싱 트리 중에서 불필요한 부분을 제외한 결과물이다.Symbol
로 보고 규칙을 정의한다. 타입 시스템은 바인더를 통해 각 선언들을 추론할 수 있다.*.ts
같은 타입스크립트 파일을 *.js
, *.d.ts
, *.js.map
유형의 파일로 생성하는 기능을 수행한다.import
문이나 <reference path="">
같은 외부 호출 선언이 존재할 경우, 참조 가능한 파일을 가져와 정렬된 목록을 생성한다. 컴파일러는 전처리기부터 생성된 목록을 사용하여 파일을 호출하고 컴파일을 수행한다.tsserver
는 독립된 서버이며 타입스크립트 컴파일러와 언어 서비스를 캡슐화하고, JSON 프로토콜을 통해 이를 외부에 공개한다. Node 기반에서 실행이 가능하다.// num 변수의 타입을 지정하지 않았으나 해당 심벌은 number로 추론되었음을 편집기에서 확인 가능.
let num = 10;
// add 함수의 return 값에 대한 타입을 지정하지 않았으나 number로 추론되었음을 확인 가능.
function add(a: number, b: number) {
return a + b;
}
function sayHi(msg: string | null) {
if (msg) {
console.log(msg); // 여기서는 msg 가 string 으로 추론되었음을 확인할 수 있다.
}
}
Go to Definition
이라는 옵션을 제공하는데, 이를 통해 TS에 포함된 DOM 선언 타입인 lib.dom.d.ts
파일로 이동된다.// lib.dom.d.ts 내의 fetch 함수에 대한 타입 선언
declare function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
type RequestInfo = Request | string;
declare var Request: {
prototype: Request;
new (input: RequestInfo, init?: RequestInit): Request;
};
*.d.ts
를 찾아서 연구하는 것도 좋다.Axios
라이브러리에서 제공하는 index.d.ts
내부에서도 Axios 와 관련된 선언 타입 정보를 모두 제공하였다.export interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: AxiosResponseHeaders;
config: AxiosRequestConfig<D>;
request?: any;
}
// 이를 직접 활용하여 필자가 작성한 GET 유틸 함수
export async function getAsync<T, D>(url: string, config?: AxiosRequestConfig): APIResult<T> {
try {
const response = await API.get<T, AxiosResponse<T, D>, D>(url, {
responseType: 'json',
...config,
});
return { isSuccess: true, result: response.data };
} catch (err) {
return { isSuccess: false, result: preProcessError(err) };
}
}
즉, 어느 범주까지 할당이 가능하니~ 에 대한 여부라고 필자는 이해하였다.
const x: number = 0; // 값 0 은 타입 number 의 원소이다. 즉 number 집합에 속한다.
const y: never = 'never'; // never 집합은 공집합이다. 어떠한 원소도 가지지 않는다.
const z:
unknown
이며 최하위 타입 은 never
이다. unknown
은 어떤 값이던 가질 수 있고, never
는 어떤 값도 가질 수 없다.type A = 'A'; // 'A' 라는 값만 포함하는 리터럴 타입.
type B = 'B'; // 'B' 라는 값만 포함하는 리터럴 타입.
type AB = A | B; // 'A' 값 또는 'B' 값을 가질 수 있는 유니온 타입.
|
를 넣어 하나로 묶는다.type C = 'C';
const a: AB = 'A'; // 'A' 형식은 집합 {'A', 'B'} 의 원소다.
const c: AB = 'C'; // 'C' 형식은 AB 에 할당할 수 없다. 집합 {'A', 'B'} 의 원소가 아니기 때문.
A
는 집합 {'A', 'B'}
에 포함되지만 C
는 그렇지 않기에 에러를 띄웠다.&
을 넣어 하나로 묶는다.interface Person {
name: string;
}
interface LifeSpan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & LifeSpan;
// 두 인터페이스 내의 필요 속성을 모두 가졌으므로 정상적으로 타입이 체크되었다.
const ps: PersonSpan = {
name: 'Baik Gwangin',
birth: new Date('1999/01/26'),
};
Person
과 LifeSpan
인터페이스는 서로 공통된 속성이 없으나, 타입 연산자는 인터페이스의 속성이 아닌 값의 집합 에 적용된다.ps
에 할당된 객체에는 Person의 속성인 name
과 LifeSpan 의 속성인 birth
가 모두 있어 정상적으로 타입이 적용된다.name
과 birth
속성이 아닌 다른 속성도 할당될 수 있다. 하지만 TS 에서 지원하는 잉여 속성 체크 에서는 이것을 지적하기에 간과할 수 있다.interface Person {
name: string;
}
interface LifeSpan extends Person {
birth: Date;
death?: Date;
}
// LifeSpan 인터페이스는 Person 인터페이스에서 확장되었으므로 name 속성을 가져야 한다.
const ps: LifeSpan = {
name: 'Baik Gwangin',
birth: new Date('1999/01/26'),
};
extends
키워드는 ~ 의 부분집합 이라는 의미로 해석이 가능하다.name
속성을 가져야 하고, birth
속성까지 가져야 비로소 성립이 된다.keyof (A & B)
= (keyof A)
| (keyof B)
keyof (A | B)
= (keyof A)
& (keyof B)
keyof
는 항상 접근 가능한 유형의 키 를 반환한다. 따라서 인터섹션 타입의 경우 A 와 B 에 속한 속성을 모두 갖춰야 한다.interface Person {
name: string;
}
interface Human {
name: string;
birth: Date;
death?: Date;
}
type K = keyof (Person | Human); // "name"
type I = keyof (Person & Human); // keyof Human : "name" | "birth" | "death"
name
이라는 공통 속성이 있지만, 만약 없다면 never
타입으로 반환될 것이다.interface Vector1D {
x: number;
}
interface Vector2D {
x: number;
y: number;
}
interface Vector3D {
x: number;
y: number;
z: number;
}
Vector3D
는 Vector2D
의 서브타입이고, Vector2D
는 Vector1D
의 서브타입이다.Vector3D
는 Vector2D
를 상속 받았고, Vector2D
는 Vector1D
를 상속 받았다고 할 수도 있다.// 타입 K 는 string 을 상속 받았다. 즉 string 의 부분 집합이다.
function getKey<K extends string>(val: any, key: K) {}
getKey({}, 'x'); // 정상, 'x' 는 string을 상속, 즉 string의 부분집합.
getKey({}, 111); // 오류, 111 는 string을 상속하지 않음, string의 부분집합이 아님.
extends
키워드는 제네릭 타입 내에서 한정자로 쓰이며, 여기서는 ~의 부분 집합 의 개념으로도 사용된다.type StringDate = string | Date;
type StringNumber = string | number;
type Inter = StringDate & StringNumber; // string
StringDate
와 StringNumber
는 서로 상속 관계가 아니지만 인터섹션을 통한 교집합을 명확히 찾아낼 수 있다.const list = [1, 2]; // type : number[]
// number[]' 형식은 '[number, number]' 형식에 할당할 수 없습니다.
// 대상에 2개 요소가 필요하지만, 소스에 더 적게 있을 수 있습니다.
const tuple: [number, number] = list;
number[]
은 [number, number]
타입의 부분 집합이 아니지만 그 반대는 가능하다.// 타입 공간에서의 Cyclinder 는 인터페이스로서의 기능을 한다.
interface Cyclinder {
radius: number;
height: number;
}
// 값 공간에서의 Cyclinder 는 함수로서의 기능을 한다.
const Cyclinder = (radius: number, height: number) => ({ radius, height });
function caculateVolume(shape: unknown) {
if (shape instanceof Cyclinder) {
return shape.radius; // '{}' 형식에 radius 가 없습니다.
}
}
instanceof
는 JS의 런타임 연산자이고 값에 대한 연산을 진행하기 때문에 타입이 좁혀지지 않았다.as
연산자를 통한 단언에 나온다.class Cyclinder {
radius = 1;
height = 1;
}
function caculateVolume(shape: unknown) {
if (shape instanceof Cyclinder) {
return shape.radius; // 정상, 클래스는 현재 타입으로 사용되고 있다.
}
}
typeof
는 값을 읽어 타입스크립트 타입을 반환시킨다.typeof
는 자바스크립트 런타임의 typeof
연산자 가 된다. 따라서 대상 심벌의 런타임 타입을 가리키는 문자열을 반환한다.interface Person {
first: string;
last: string;
}
const person: Person = { first: 'Baik', last: 'Gwangin' };
type T1 = typeof person; // Person
const v1 = typeof person; // "object"
T1
은 타입의 관점에서 person 변수의 값을 읽어 반환된 타입인 Person 이다.v1
에는 person 이 객체이므로 "object"
문자열이 반환되었다.class Cyclinder {
radius = 1;
height = 1;
}
const v = typeof Cyclinder; // "function"
type T = typeof Cyclinder; // typeof Cyclinder
type T2 = InstanceType<typeof Cyclinder>; // Cyclinder
typeof
의 결과가 "function"
으로 나온다.InstanceType
제네릭을 활용하면 된다.interface Person {
first: string;
last: string;
}
const person: Person = { first: 'Baik', last: 'Gwangin' };
// 값의 관점에서는 대괄호를 사용하던, 속성 연산자 (.) 를 사용하던 괜찮다.
const first = person['first']; // person.first
// 타입의 관점에서는 반드시 대괄호를 사용하여 속성에 접근해야 한다.
type First = Person['first']; // string
this
키워드&
, |
연산자&
, |
연산자는 AND, OR 비트 연산이다.&
, |
연산자는 인터섹션과 유니온 타입이다.const
키워드const
키워드는 새로운 변수를 선언하기 위해 쓰인다.as const
키워드는 리터럴, 혹은 리터럴 표현식의 추론된 타입을 변환한다.extends
키워드extends
키워드는 서브 클래스를 지정하여 상속 관계를 표현한다.extends
키워드는 제네릭 타입의 한정자, 혹은 서브 타입을 지정한다.in
키워드in
키워드는 for - in
구문 혹은 객체의 속성 포함 여부 등에 쓰인다.in
키워드는 mapped type, 즉 매핑된 키워드에서 쓰인다.interface Person {
name: string;
}
const alice: Person = { name: 'Alice' };
const bob = { name: 'bob' } as Person;
// (name: Person) 으로 적을 경우 name을 Person 타입으로 인식, 반환 타입 없음
const people = ['alice', 'bob', 'jan'].map((name): Person => {
const person = { name };
return person;
});
const button = document.getElementById('button'); // HTMLElement | null
const button1 = document.getElementById('button')!; // HTMLElement 로 단언
interface Person {
name: string;
}
const button = document.getElementById('button');
const el = button as Person; // 'HTMLElement | null' 형식을 'Person' 형식으로 변환한 작업은 실수일 수 있습니다. 두 형식이 서로 충분히 겹치지 않기 때문입니다.
!
를 접미사로 붙이면 해당 값이 null
이 아니라는 단언문으로 해석된다.unknown
을 사용함으로서 해결은 가능하다.replace
, charAt
같이 문자열과 연관된 메서드를 자유롭게 사용할 수 있다.const string = 'String';
string.charAt(3); // 메서드를 사용할 수 있다.
string
원시형의 경우 String
래핑 객체로 변환되어 메소드를 호출하고, 마지막에 래핑한 객체를 버린다.this
는 원시 타입의 값이 아닌 래핑된 객체이며, 이는 원시 타입의 값과 거의 동일하게 동작한다.var x = 'hello';
x.language = 'English';
console.log(x.language); // undefined
'hello' === 'hello'; // true
'hello' === new String('hello'); // false
new String('hello') === new String('hello'); // false
string
기본형과 String
래퍼 객체가 완전히 동일하게 동작하는 것은 아니다.x
에 실제로 language
속성을 넣었다고 생각했지만, 실제로는 래핑된 객체에 속성을 추가한 뒤 이를 버린 것이다.interface Room {
numDoors: number;
celingHeightFt: number;
}
const r: Room = {
numDoors: 1,
celingHeightFt: 10,
isBooked: true, // '{ numDoors: number; celingHeightFt: number; isBooked: boolean; }' 형식은 'Room' 형식에 할당할 수 없습니다.
};
const r2: Room = r; // 정상, 오류가 발생하지 않는다.
r
에 isBooked
속성이 있어도 오류가 없어야 한다. Room
인터페이스에서 요구하는 속성들을 모두 갖추었기 때문이다.r2
) 를 도입하면 해결이 가능한데, 두 구문의 차이는 잉여 속성 체크 의 유무이다. 이는 할당 가능 검사와는 별도로 작동한다. interface Options {
title: string;
darkMode?: boolean;
}
function createWindow(options: Options) {
if (options.darkMode) {
setDarkMode();
}
}
// 개체 리터럴은 알려진 속성만 지정할 수 있지만 'Options' 형식에 'darkmode'이(가) 없습니다. 'darkMode'을(를) 쓰려고 했습니까?
createWindow({ title: 'Spider Solitaire', darkmode: true });
// 심지어는 이런 케이스도 전부 할당이 가능하다. 모두 `title` 속성을 가지고 있기 때문이다.
const o1: Options = document;
const o1: Options = new HTMLAnchorElement();
// 이 경우에는 오류가 발생한다. darkmode 는 Options 타입에 없어 잉여 속성 체크에 걸렸다.
const o3: Options = { title: 'Ski Free', darkmode: true };
Options
타입은 범위가 매우 넓기 때문에 구조적 타입 체커로는 이런 종류의 오류를 쉽게 잡기 어렵다. 단순히 title
속성만 가지고 있다면 전부 허용해버리기 때문이다.const o = { title: 'Ski Free', darkmode: true }; // darkmode 라는 잘못된 속성이 기입됨.
const o2: Options = o; // 정상
interface Options {
title: string;
[otherOptions: string]: unknown; // 인덱스 시그니쳐 사용
}
const o: Options = { title: 'Ski Free', darkMode: true }; // 정상
interface LineChartOptions {
logscale?: boolean;
invertedYAxis: boolean;
areaChart: boolean;
}
const opts = { logScale: true };
// { logScale: boolean; } 형식에 'LineChartOptions' 형식의 invertedYAxis, areaChart 속성이 없습니다.
const o: LineChartOptions = opts; // 임시 변수를 생성해도 또 체크를 진행함.
LineChartOptions
는 모든 타입이 optional 함으로 모든 객체를 포함할 수 있다.type CalculateNumber = (a: number, b: number) => number;
const add: CalculateNumber = (a, b) => a + b;
const sum: CalculateNumber = (a, b) => a - b;
const mul: CalculateNumber = (a, b) => a * b;
const div: CalculateNumber = (a, b) => a / b;
// lib.dom.d.ts
declare function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
// fetch 함수를 타입으로 사용하여 각 매개변수와 반환 값이 자동으로 추론되었다.
// 매개변수 input, init 에 별도의 타입을 지정하지 않아도 알아서 추론해준다.
const checkFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error(`Request failed : ${response.status}`);
}
return response;
};