타입스크립트에서 명명된 타입을 정의하는 방법은 타입을 쓰는방법과 인터페이스를 쓰는 방법 두 가지가 있다.
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 = '';
}
단 튜플을 구현할 때는 인터페이스보다 타입이 더 편합니다.인터페이스로 튜플과 비슷하게 구현하면 튜플에서 사용할 수 있는 concat 과 같은 메서드를 사용할 수 없습니다. 따라서 튜플은 type 키워드로 구현하는 것이 좋습니다.
튜플구현
type Pair = [number, number];
interface Tuple {
0: number;
1: number;
length: 2;
}
인터페이스는 선언 병합이 가능하다
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}) {
// do something...
}
type Point2D = {
x: number;
y: number;
}
function distance2(a: Point2D, b: Point2D) {
// do something...
}
타입확장 시 다른 인터페이스로부터 확장하자
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 타입이 너무 광범위해서 인덱스 시그니처를 사용할 때 문제가 있을때 해결방법은?
Record 사용하기
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-of
인덱스의 타입이 중요할 때 : Array.prototype.forEach
루프 중간에 멈춰야할 때 : for(;;) 루프
어떤 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용한다.
readonly 란?
값의 속성을 읽기 전용으로 설정해주는 Typescript의 타입시스템 기능이다.
함수가 매개변수로 받는 값을 변경없이 그대로 사용해야할 때 적합하며
외부 클래스나 함수에서도 호출이 가능하지만 값의 변경은 불가능하므로 내부에서 미리 값을 초기화 해줘야합니다.
readonly가 필요한 이유?
사실 readonly를 사용하지 않더라도 JS는 암묵적으로 매개변수를 변경하지 않는다고 가정한다.
하지만 이러한 방법은 항상 명확하지는 않기 때문에 readonly를 명시적으로 선언하는 것이
컴파일러와 코드를 읽는 사람에게 좋다.
다음은 타입 체커가 동작하도록 개선한 코드인데, 핵심은 매핑된 타입과 객체를 사용하는 것이다. [k in keyof ScatterProps]는 타입 체커에게 REQUIRES_UPDATE가 ScatterProps와 동일한 속성을 가져야 한다는 정보를 제공한다.
interface ScatterProps {
// The data
xs: number[];
ys: number[];
// Display
xRange: [number, number];
yRange: [number, number];
color: string;
// Events
onClick: (x: number, y: number, index: number) => void;
}
const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = {
xs: true,
ys: true,
xRange: true,
yRange: true,
color: true,
onClick: false,
};
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
let k: keyof ScatterProps;
for (k in oldProps) {
if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
return true;
}
}
return false;
}