
솔직히 개인적으로 제일 흥미롭게 읽은 아이템 😎
타입스크립트에는 타입을 정의할 수 있는 방법이 두가지 있다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
물론 인터페이스 대신 클래스를 사용할 수 있지만, 클래스는 값으로도 쓰일 수 있기 때문에 이 점을 유의해야 한다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
type TDict = { [key: string]: string };
interface IDict {
[key: string]: string;
}
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
type TFn = (x: number) => string;
interface IFn {
(x: number): string;
}
const toStrT: TFn = x => '' + x; // OK
const toStrI: IFn = x => '' + x; // OK
type TFnWithProperties = {
(x: number): number;
prop: string;
}
interface IFnWithProperties {
(x: number): number;
prop: string;
}
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
type TPair<T> = {
first: T;
second: T;
}
interface IPair<T> {
first: T;
second: T;
}
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
interface IStateWithPop extends TState {
population: number;
}
type TStateWithPop = IState & { population: number; };
다만, 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다.
복잡한 타입을 확장하고 싶다면 타입과 &을 사용해야 한다.
튜플도 마찬가지로 타입을 사용하면 간단하게 사용할 수 있는데, 인터페이스로 튜플과 비슷하게 구현하게 되면 튜플에서 사용할 수 있는 concat 같은 메서드를 사용할 수 없다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StateI implements IState {
name: string = '';
capital: string = '';
}
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}; // OK
이렇게 속성확장하는 것을 선언 병합(declaration merging)이라고 한다.
그럼 타입과 인터페이스 중, 뭘로 개발해야하냐!
라고 물어본다면, 처음 개발할 때 써왔거나 혹은 기존 코드베이스에 따라 일관되게 사용하는게 좋다.
API의 타입을 선언해야 한다면 인터페이스가 더 좋을 수 있다. API가 변경되는 일이 있다면 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하다.
반대로, 프로젝트 내부적으로 사용하는 타입에 대해서는 이런 병합이 발생하면 안되므로 타입을 사용하는게 좋다.
같은 코드를 반복하지 말라는 DRY(don't repeat yourself)원칙으로 타입 반복을 줄여보았다는 내용이 이 챕터의 대부분 내용이다.
Pick// AS-IS
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
// TO-BE 1
type TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
};
// TO-BE 2 - 매핑된 타입을 사용하기
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식으로 타입을 지정하며, Pick과 같다. 아래 코드를 보면, 부분집합으로 선택한 것과 완전히 동일하진 않은 것을 알 수있다. 이 점 유의해서 구분해서 사용하면 좋을 것 같다.
interface SaveAction {
type: 'save';
// ...
}
interface LoadAction {
type: 'load';
// ...
}
type Action = SaveAction | LoadAction;
type ActionType = Action['type']; // Type is "save" | "load"
type ActionRec = Pick<Action, 'type'>; // {type: "save" | "load"}
keyof, Partial
interface Options {
width: number;
height: number;
color: string;
label: string;
}
type OptionsUpdate = {[k in keyof Options]?: Options[k]};
type OptionsKeys = keyof Options;
// Type is "width" | "height" | "color" | "label"
매핑된 타입([k in keyof Options])은 순회하며 Options내 k과 해당하는 속성이 있는지 찾는다. ?는 각 속성을 선택적으로 만들고, Partial과 같은 방식을 적용해서 OptionsUpdate의 타입을 만들었다.
typeofconst INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
type Options = typeof INIT_OPTIONS;
값을 타입으로 사용함으로써 타입의 반복을 줄였는데, 이 때 선언의 순서에 주의해야 한다. 타입 정의를 먼저 하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다. 그렇게 해야 타입이 더 명확해지고, 예상하기 어려운 타입 변동을 방지할 수 있다.
extends제너릭 타입은 타입을 위한 함수와 같다. 제너릭 타입에서 매개변수를 제한할 수 있는 방법은 extends를 사용하는 것이다.
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T];
const couple1: DancingDuo<Name> = [
{first: 'Fred', last: 'Astaire'},
{first: 'Ginger', last: 'Rogers'}
]; // OK
const couple2: DancingDuo<{first: string}> = [
// ~~~~~~~~~~~~~~~
// Property 'last' is missing in type
// '{ first: string; }' but required in type 'Name'
{first: 'Sonny'},
{first: 'Cher'}
];
type Rocket = {[property: string]: string};
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
}; // OK
[property: string]: string이 인덱스 시그니처다.
다만, 이렇게 타입 체크를 하게 되면 단점이 4가지가 된다.
name 대신 Name을 쓰더라도 허용된다.{}타입도 허용하게 된다.이런 단점 때문에 타입이 명확하게 필요한 곳 보다, 어떤 데이터가 들어올 지 예상할 수 없는 동적 데이터를 표현할 때 사용한다.
예를 들면, CSV파일처럼 행과 열에 이름이 있고, 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우다. 하지만 이 때 열 이름을 알고 있는 특정한 상황이 있다면, 미리 선언해 둔 타입으로 단언문을 사용한다.
Map연관 배열(associative array)의 경우, 객체에 인덱스 시그니처를 사용하는 대신 Map타입을 사용하는 것을 고려할 수있다.
이 타입의 구체적인 예시는 ITEM 58에서 볼 수 있다.
Record어떤 타입에 가능한 필드가 제한되어 있는 경우라면, 인덱스 시그니처로 모델링하지 말아야 한다.
// AS-IS
interface Row1 { [column: string]: number } // Too broad
interface Row2 { a: number; b?: number; c?: number; d?: number } // Better
type Row3 =
| { a: number; }
| { a: number; b: number; }
| { a: number; b: number; c: number; }
| { a: number; b: number; c: number; d: number };
// TO-BE 1
type Vec3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
// TO-BE 2
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type ABC = {
// a: number;
// b: string;
// c: number;
// }
number인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기자바스크립트에서 객체란 키/값 쌍의 모음이고, 키는 보통 문자열이며 그 값은 어떤 것이든 될 수 있다.
배열은 객체고, 숫자 인덱스를 사용하는 것이 당연하다.
하지만 인덱스들은 문자열로 변환되어 사용되고 그 문자열 키를 사용해도 역시 배열의 요소에 접근할 수 있다.
const x = [1, 2, 3]
console.log(x[0]) // 1
console.log(x['1']) // 2
console.log(Object.keys(x)) // ['0', '1', '2']
Object.keys(x)로 키를 나열하면, 키가 문자열로 나온다. 타입 스크립트는 이런 혼란을 바로 잡기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식한다.
인덱스 시그니처가 number로 표현되어 있다면 입력한 값이 number여야 한다는 것을 의미하지만, 실제 런타입에 사용되는 키는 string타입이다. 게다가 number로 지정한다면 이 숫자 속성이 어떤 특별한 의미를 지닌다는 오해를 불러 일으킬 수 있다.
따라서 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, 또는 ArrayLike타입을 사용하는 것이 좋다.
배열의 타입이 불확실 하다면, (대부분의 브라우저와 자바스크립트 엔진에서)
for-in루프는for-of또는 C 스타일for루프에 비해 몇 배나 느리다.
readonly 사용하기function arraySum(arr: number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) {
sum += num;
}
return sum;
}
function arraySum(arr: readonly number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) {
// ~~~ 'pop' does not exist on type 'readonly number[]'
sum += num;
}
return sum;
}
readonly number[]와 number[]의 차이
length를 읽을 수 있지만, 바꿀수는 없다(배열을 변경함).pop을 비롯한 다른 메서드를 호출할 수 없다.number[]는 readonly number[]보다 기능이 많기 때문에, readonly number[]의 서브타입이 된다.매개변수를 readonly로 선언하면?
readonly 배열을 매개변수로 넣을 수 있다.만약 함수가 매개변수를 변경하지 않고도 제어가 가능하다면 readonly로 선언하면 된다. 그런데 어떤 함수를 readonly로 만들면, 그 함수를 호출하는 다른 함수도 모두 readonly로 만들어야 한다.
➡ 인터페이스를 명확히 하고 타입 안정성을 높일 수 있기 때문에 꼭 단점이라고 볼 순 없다.
단, 다른 라이브러리에 있는 함수를 호출하는 경우라면 타입 단언문을 사용하는 것이 좋다.
readonly는 얕게(shallow)동작한다.
매핑된 타입을 사용해서 관련된 값과 타입을 동기화하고, 인터페이스에 새로운 속성을 추가할 때 선택을 강제하도록 매핑된 타입을 고려해야 한다.
실패에 닫힌(fail close) 방법은 오류 발생 시에 적극적으로 대처하는 방향을 말한다. 말 그대로 방어적, 보수적(conservative) 접근법이다. 반대로 실패에 열린 방법은 오류 발생 시에 소극적으로 대처하는 방향이다. 보안과 관련된 곳이라면 실패에 닫힌 방법을, 기능에 무리가 없고 사용성이 중요한 곳이라면 실패에 열린 방법을 써야 할 것이다.