타입스크립트에서 명명된 타입을 정의하는 방법은 두 가지가 있다.
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
✔ 공통점
type Tdict = { [key: string]: string };
interface IDict {
[key: string]: string;
};
type TFn = (x: number) => string;
interface IFn {
(x: number) => string;
}
type TPair<T> = {
first: T;
second: T;
}
interface IPair<T> {
first: T;
second: T;
}
interface IStateWithPop extends TState {
population: number;
}
type TStateWithPop = IState & { population: number; };
IStateWithPop과 TStateWithPop은 동일하다.
❗ 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지 못한다. 복잡한 타입을 확장하고 싶으면 타입과 &를 사용해야 한다.
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StateI implements IState {
name: string = '';
capital: string = '';
}
✔ 차이점
type Pair = [number, number];
interface Tuple {
0: number;
1: number;
length: 2;
}
❗ 인터페이스로 튜플과 비슷하게 구현하면 튜플에서 사용할 수 있는 concat 과 같은 메서드를 사용할 수 없다. 따라서 튜플은 type 키워드로 구현하자
interface IState {
name: string;
}
interface IState {
capital: string;
}
const ex: IState = {
name: 'yongwoo';
capital: 'seoul'
}; // 정상
👉 타입 선언 파일을 작성할 때는 선언 병합을 지원하기 위해 반드시 인터페이스를 사용해야 하며 표준을 따라야 한다.
복잡한 타입이라면 고민할 것도 없이 타입 별칭을 사용한다. 그러나 타입과 인터페이스, 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 봐야 한다.
타입 중복은 코드 중복만큼 많은 문제를 발생시키므로 타입에도 DRY원칙을 적용하자
✔ 타입에 이름을 붙이자
function distance(a: {x: number, y: number}, b: {x: number, y: number}) { /* ... */ }
interface Point {
x: number;
y: number;
}
function distance(a: Point, b: Point){ /* ... */ }
✔ 함수가 같은 타입 시그니처를 공유하면 분리하자
function get(url: string, opts: Options): Promise<Response> { /* ... */ }
function post(url: string, opts: Options): Promise<Response> { /* ... */ }
type HTTPfunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPfunction = (url, opts) => { /* ... */ };
const post: HTTPfunction = (url, opts) => { /* ... */ };
✔ 타입확장 시 다른 인터페이스로부터 확장하자
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirth extends Person {
birth: Date;
}
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
✔ 인덱싱을 통해 타입 반복을 줄이자
interface SaveAction {
type: 'save';
}
interface LoadAction {
type: 'load';
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // 👎 타입이 반복됨
type ActionType = Action['type']; // 👍 유니온 인덱싱을 통한 반복제거
타입스크립트에서는 타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다.
type Rocket = { [property: string]: string };
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
};
👎 위와 같은 타입 체크의 단점
interface Rocket {
name: string;
varinat: string;
thrust: number;
}
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: 100,
};
위와 같은 단점을 인터페이스로 설정함으로써 해결할 수 있다.
인덱스 시그니처는 동적 데이터(런타임 때까지 객체의 속성을 알 수 없음)를 표현할 때 사용한다.
ex) CSV 파일처럼 헤더 행에 열이름이 있고, 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우
일반적인 상황에서 열 이름이 무엇인지 미리 알 방법이 없다. 따라서 이럴때는 인덱스 시그니처를 사용한다. 알고 있을 때는 미리 선언해 둔 타입으로 단언문을 사용한다.
❗ 어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링하면 안된다.
✔ string 타입이 너무 광범위해서 인덱스 시그니처를 사용할 때 문제가 있을때 해결방법
type Ved3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
Record는 키 타입에 유연성을 제공하는 제너릭 타입이다. 특히, string의 부분 집합을 사용할 수 있다.
type Ved3D = { [k in 'x' | 'y' | 'z']: number };
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
가능하다면 인터페이스, Record, 매핑된 타입 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋다.
interface Array<T> {
// ...
[n: number]: T;
}
const xs = [1, 2, 3];
const x1 = xs['1']; // ❌ 에러 (인덱스 식이 number형식이여야 함)
👍 타입 체크 시점에 오류를 잡을 수 있어서 유용함
배열 순회 방법
❗ 타입이 불확실하다면 for-in 루프는 for-of 또는 C스타일 for 루프에 비해 몇 배나 느림
어떤 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용한다.