타입스크립트는 자바스크립트에 타입 문법을 추가한 언어이다.
자바스크립트로 프로젝트를 진행할때, 런타임에서 오류가 나는 경우가 많은데, 타입스크립트는 런타임에서 발생할 수 있는 오류를 컴파일에서 잡아줌으로써, 런타임에서 에러가 발생할 확률을 줄여주게 된다.
타입스크립트를 사용하면 런타임에서 모든 오류를 해결해주는 것이 아니라, 런타임에서 타입관련하여 발생할 수 있는 오류를 컴파일에서 발견할 수 있는 것이다!
본 포스팅은 타입스크립트 핸드북을 읽으면서, 들었던 의문과 답변에 대해 정리하였다.
typescript의 컴파일러는 tsc이다. tsc를 실행하면, .ts파일을 .js로 변환시켜준다. tsc는 대부분의 경우 사용자가 typescript보다 잘 알고 있을 것이라는 전제하에, ts파일에서 타입관련 오류가 있어도, 그대로 js 파일로 바꿔주게 된다.
strictNullChecks
: null과 undefined 값을 참조하는 것을 방지하는 옵션
noImplicitAny
: 타입을 any로 추론하게되면 오류를 발생시키는 옵션
interface와 type은 단독적으로 사용되었을때 매우 유사하게 동작한다.
다음과 같이 interface는 한번 선언된 이후에, 또다시 선언을 하게되면 새로운 field를 추가하는 것으로 이해한다.
interface Window {
title: string
}
interface Window {
ts: TypeScriptAPI
}
그러나 type의 경우에는 한번 선언된 이후에 변경될수 없기 때문에, 다음과 같은 상황을 오류로 인식한다.
type Window = {
title: string
}
type Window = {
ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
index signatures을 선언한 경우, 독립적으로 다른 property를 추가할때, index signature와 같은 key type을 사용하는 경우, value의 type도 index signature의 value type과 동일해야한다.
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
extend는 확장하다라는 의미이다.
interface A {
name : string,
}
interface B extends A{
age : number
}
위와 같이 interface A와 B가 있을때, 필자는 B extends A 를 B가 A를 확장했다는 의미로 해석하고, B가 A를 포함하는 느낌으로 이해했다.
그러나 이는 완전히 반대였고, B extends A는 A가 B를 포함하는 것이고, B는 A의 부분집합이라고 볼 수 있는 것이다.
즉, extend를 사용하면 오히려 interface가 더 구체적으로? 제약조건이 추가로 생겨서 narrowing되는 느낌인 것이다.
이러한 의문은 제네릭 <T extends U>
에도 적용할 수 있다.
<T extends string | number>
에서, T는 string 또는 number가 될 수 있는 이유는
T는 string | number
의 부분집합이여야 하기 때문이다.
다음과 같이 interface의 extend를 할때, 같은 key값을 가지는 property가 있는 경우 conflict error가 발생한다.
interface NumberToStringConverter {
convert: (value: number) => string;
}
interface BidirectionalStringNumberConverter extends NumberToStringConverter {
convert: (value: string) => number;
}
error TS2430: Interface 'BidirectionalStringNumberConverter' incorrectly extends interface 'NumberToStringConverter'.
그러나 intersection은 두 property를 combine한다
type NumberToStringConverter = {
convert: (value: number) => string;
}
type BidirectionalStringNumberConverter = NumberToStringConverter & {
convert: (value: string) => number;
}
const converter: BidirectionalStringNumberConverter = {
convert: (value: string | number) => {
return (typeof value === 'string' ? Number(value) : String(value)) as string & number; // type assertion is an unfortunately necessary hack.
key of
operator는 object의 key를 literal union으로 만들어준다.
type Person = {
name : string,
age : number
}
type A = keyof Person
///A = 'name' | 'age'
index signature에서 keyof
operator를 사용할 경우 number이 자동적으로 포함된다.
obj[0]
으로 접근하는 것은 obj["0"]
과 동일하기 때문이다.
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
type M = string | number
in key of를 사용하여 readonly를 속성을 제거하는 예제
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
type UnlockedAccount = {
id: string;
name: string;
}
mapped type을 이용해서 optional 속성을 제거하거나, exclude와 함께 사용하여 특정 property만들 제거한 type을 생성하거나, conditional type와 함께 사용할 수도 있다.
keyof typeof 를 사용해서 object를 enum처럼 사용할 수 있다.
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
EDirection.Up;
(enum member) EDirection.Up = 0
ODirection.Up;
(property) Up: 0
// Using the enum as a parameter
function walk(dir: EDirection) {}
// It requires an extra line to pull out the keys
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right);
함수의 return type으로 arg is {type}
을 주게되면, Type Guard로 인식한다.
function getWheelOrMotor(machine : any) : number{
if(isCar(machine)){
return machine.wheel;
}else if(isBoat(machine)){
return machine.motor;
} else return -1;
}
function isCar (arg : any) : arg is Car{
return arg.type==='CAR';
}
function isBoat(arg : any) : arg is Boat{
return arg.type==='Boat';
}
T의 모든 property를 optional로 변경
type Partial<T> = {
[P in keyof T]? : T[P];
};
T의 key 중에서, K에 해당하는 속성을 선택하여 반환
type Pick<T , K extends keyof T> = {
[P in K] : T[P];
};
Union T에서 U 와 겹치는 부분을 추출
type Extract<T,U> = T extends U ? T : never;
Union T에서 null과 undefined를 제외한 타입을 반환
type NonNullable<T> = T extneds null | undefined ? never : T;
함수 T의 매개변수 타입을 배열로 반환
type Parameters<T extends (...args : any) => any> = T extends (...args : infer P) => any ? P : never;
함수 T의 return 타입을 반환
type ReturnType<T extends (...args : any) =>any> = T extends (...args : any) => infer R ? R : any;
공변성 : A가 B의 서브타입이면, T<A>는 T<B>의 서브타입이다.
반공변성 : A가 B의 서브타입이면, T<B>는 T<A>의 서브타입이다.
이변성 : A가 B의 서브타입이면, T<A> -> T<B>도 되고, T<B> -> T<A>도 되는경우
타입스크립트에서는 일반적으로 공변성이 적용된다.
let stringArray : Array<string> = [];
let array : Array<string | number | boolean> = [];
array = stringArray /// 가능
stringArray = array /// 불가능
string은 string | number | boolean의 서브타입이므로, Array<string> 은 Array<string | number | boolean>의 서브타입임이 성립하는 경우와 같은 경우를 공변적이라고 할 수 있다.
그러나 tsconfig에서 strictFunctionTypes
가 true일때 함수의 매개변수타입은 반공변적이 된다.
type A = (x : string | number) => number;
type B = (x : string) => number;
function a(x: string | number) : number {
return 0;
}
function b(x : string) : number{
return 0;
}
let bb : B = a; //가능
let aa : A = b; //불가능
매개변수로 string만을 처리하는 함수에 string | number을 처리할수 있는 함수를 대입하는 것은 가능하지만, string | number을 처리할 수 있는 함수에 string만을 처리하는 함수를 대입하는 것은 부족하기 때문에 반공변성이 적용된다.
또한, strictFunctionTypes
가 false일때 함수의 매개변수 타입은 이변적이된다.
이변성을 가질때는 함수끼리 매개변수가 달라도 서로 대입이 가능하다.