같은 코드를 반복하지 말라는 DRY(don’t repeat yourself)원칙이 있다. 이 아이템은 타입 반복을 줄여본다는 내용이 대부분이다.
타입 중복 또한 코드 중복만큼 피해야 한다. 타입이 에서 공유된 패턴을 제거하는 일은 js에서 중복된 코드를 제거하는 것보다 생소하게 느껴질 수 있기 때문에 타입 간에 매핑하는 방법을 익힌다면 ts에서도 DRY 원칙을 지킬 수 있다.
function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
//위 코드에서 타입에 이름을 붙여 수정하면
interface Point2D{
x: number;
y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }
interface Person {
name: string;
age: number;
weight: number;
}
type TopPerson = {
userId: Person['name'];
age: Person['age'];
weight: Person['weight'];
}
type TopPerson = {
[k in 'name' | 'age' | 'weight']: Person[k] // 매핑된 타입
}
type TopPerson = Pick<Person, 'name' | 'age' | 'weight'>; // 제너릭 타입
[k in T]은 매핑된 타입은 배열의 필드를 순회하는 것 과 같은 방식이다.
아래 코드처럼 태그된 유니온에서 type 속성의 타입을 꺼내고 싶은 경우에 반복이 발생할 수 있다
interface SaveAction {
type: 'save';
// ...
}
interface LoadAction {
type: 'load';
// ...
}
type Action = SaveAction | LoadAction; // 태그된 유니온
type ActionType = 'save' | 'load'; // 타입의 반복 !
이 경우 Action 유니온을 인덱싱 하여 타입 반복 없이 ActionType 을 정의할 수 있다.
type ActionType = Action['type']; // 타입은 'save' | 'load'
type ActionType = Pick<Action, 'type'>; // 제네릭 타입
아래 코드는 인스턴스가 생성되고 난 다음 프로퍼티가 업데이트 되는 클래스를 정의하는 경우이다.
이 때 업데이트시 대부분의 타입들이 선택적 필드가 된다.
interface Options {
width: number;
height: number;
color: string;
}
interface OptionsUpdate { // 기존의 Options타입과 동일하면서 대부분이 선택적 필드이다.
width?: number;
height?: number;
color?: string;
}
class UIWidget {
constructor (init: Options) { /* */ }
update(options: OptionsUpdate) { /* */ }
}
매핑된 타입과 keyof 를 사용하면 Options 로부터 OptionsUpdate 를 만들 수 있다.
type OptionsUpdate = { [k in keyof Options]?: Options[k] };
keyof 는 타입을 받아서 속성 타입의 유니온 을 반환한다.
type OptionsKeys = keyof Options; // 'width' | 'height' || 'color'
const INIT_OPTIONS = {
width: 500,
height: 550,
color: '#00FF00',
label: 'VGA',
};
type Options = typeof INIT_OPTIONS;
/**
* 다음과 동일
* interface Options{
* width: number;
* height: number;
* color: string;
* };
* /
여기서 사용된 typeof 는 런타임 연산자가 아니라 타입스크립트 단계에서 연산되어 강력한 타입 표현이 가능하다.
그러나 값으로부터 타입을 만들어 낼 때는 선언 순서에 주의해야한다. 타입 정의를 먼저 하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다. 그렇게 해야 타입이 더 명확해지고 예상하기 어려운 타입 변동을 방지할 수 있다.
함수나 메서드의 반환 값에 명명된 타입을 만들고 싶은 경우 ReturnType 제네릭을 사용하면 된다. 표준 라이브러리에 이런 일반적 패턴의 제네릭 타입이 정의되어 있다.
function getUserInfo(userId: string) {
// ...
return {
userId,
name,
age,
height,
weight,
}
}
type UserInfo = ReturnType<typeof getUserInfo>; // getUserInfo 의 타입이 적용됨
매핑된 타입을 생성해주는 Pick 제네릭
간단하게 표현하면 다음과 같다.
type Pick<T, K> = { [k in K]: T[k]};
정확히 표현하면 extends 를 사용해 제네릭 매개변수의 타입을 제한해주어야 한다.
type Pick<T, K extends keyof T> = { [k in K]: T[k] };
type TopState = Pick<State, 'name' | 'age' | 'weight'>;
인덱스 시그니처는 객체의 키, 밸류를 다음과 같이 표현한 것이다.
type Something = {[property: string]: string};
인덱스 시그니처는 유연하게 타입 매핑을 표현할 수 있다.
type Person = { [property: string]: string };//--> 인덱스 시그니처
const hee: Person = {
name: 'hee',
job: 'developer',
hobby: 'drawing'
}
인덱스 시그니처는 세 가지 의미를 가지고 있다.
그러나 이렇게 타입체크를 하게 되면 단점이 있는데
interface Person {
[key: string]: string;
name: string;
age: number;
}
// 'age' 형식의 'number' 속성을 'string' 인덱스 유형 'string'에 할당할 수 없습니다.
위와 같은 인덱스 시그니처는 런타임 때까지 객체의 속성을 알 수 없을 경우에만 사용한다. 또 안전한 접근을 위해 undefined 추가를 고려할 수 있지만 이 부분을 체크하는 코드가 필요하다.
대안으로
Record 제네릭 타입 : 키 타입에 유연성을 제공하는 제네릭 타입이다.
Record<’x’ | ‘y’ | ‘z’, number>
→ key 타입에 유연성 제공(값은 number)
매핑된 타입: 키마다 별도의 타입 을 사용할 수 있다.
{ [k in ‘x’ | ‘y’ | ‘z’]: number } or { [k in ‘a’ | ‘b’ | ‘c’]: k extends ‘b’ ? string: number; }
위와 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋다.
자바스크립트에서 객체란 키/값 쌍의(string, symbol) 모음이고, 키는 보통 문자열이며 그 값은 어떤 것이든 될 수 있다.
숫자나 다른 타입의 값을 키로 사용하고자 하면 자바스크립트 런타임시 문자열로 변환된다.
이는 배열도 마찬가지로 인덱스로 접근시 x[1] 인덱스가 문자열로 변환되어 x['1'] 사용된다.
타입스크립트는 이런 혼란을 잡기위해 숫자 키를 허용하고, 문자열과 다른 것으로 인식한다.
따라서 타입 체크 시점에서 배열의 인덱스에 문자열을 사용하는 오류를 잡을 수 있다.
const arr = [1, 2, 3];
const arr0 = arr[0]; // ok
const arr1 = arr['1']; // 오류
자바스크립트에서는 배열도 객체이므로 인덱스로 접근시 숫자가 문자열로 변환된다. x[1] -> x['1'] 타입스크립트는 이와 같은 혼란을 피하기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식하는데 Object.keys같은 구문은 여전히 문자열로 반환됨.
const xs = [1, 2, 3];
const keys = Object.keys(xs); // keys: string[]
for(const key in xs) {
key; // key: string
const x = xs[key]; // key가 문자열임에도 인덱스로 접근시 타입스크립트에서 오류를 발생하지 않음
}
{ 0: 1, 1: 2, 2: 3 }; // number 인덱스 시그니처 -> 숫자 인덱스를 작성하더라도 어차피 자바스크립트에서는 인덱스로 접근시 문자열로 타입을 변경하여 접근한다. 그리고 타입스크립트는 숫자 인덱스와 문자열 인덱스를 서로 다른것으로 인식하기 때문에 문제가 발생할 수 있다.
const arr = [1, 2, 3]; // 배열 -> 숫자로 인덱스할 항목을 지정하는 경우 위처럼 객체를 사용하지 않고 배열을 사용하는것이 바람직하다.
const arrLike = { // 유사 배열 객체 -> map, forEach 같은 배열 고차함수를 사용하고 싶지 않을때 유사배열 객체를 사용해라. 하지만 키는 여전히 문자열이다.
'0': 1,
'1': 2,
'2': 3,
length: 3,
};