
자바스크립트(그리고 타입스크립트)에서는 함수 문장(statement)과 함수 표현(expression)을 다르게 인식한다.
function rollDice1(sides: number): number { /* ... */ } // 문장
const rollDice2 = function(sides: number): number { /* ... */ } // 표현식
const rollDice3 = (sides: number): number => { /* ... */ } // 표현식
타입스크립트에서는 함수 표현식을 사용하는 것이 좋다.
그 이유를 알아보자.
type Add = (num: number) => number;
const add: Add = num => { /* ... */ }
function add(a: number, b: number) { return a + b; }
function sub(a: number, b: number) { return a - b; }
function mul(a: number, b: number) { return a * b; }
function div(a: number, b: number) { return a / b; }
반복되는 함수 시그니처를 하나의 함수 타입으로 통합할 수 있다.
type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;
라이브러리는 공통 함수 시그니처를 타입으로 제공하여, 공통 콜백 함수를 위한 타입 선언을 제공한다.
리액트는 함수의 매개변수에 명시하는 MouseEvent 타입 대신에, 함수 전체에 적용할 수 있는 MouseEventHandler 타입을 제공한다.
만약 다른 함수의 시그니처를 참조하려면
typeof fn을 사용하면 된다.
const checkedFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error('Request failed ' + response.status);
}
return response;
}
타입스크립트에서 명명된 타입(named type)을 정의하는 방법으로 type과 interface가 있다.
대부분의 경우 타입과 인터페이스를 아무거나 사용해도 되지만 둘 사이에 존재하는 차이를 분명하게 알고, 같은 상황에서는 동일한 방법으로 타입을 정의해 일관성을 유지할 필요가 있다.
type TPerson = {
name: string,
}
interface IPerson {
name: 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; };
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 같은 메서드들을 사용할 수 없다.
interface Person {
name: string;
}
interface Person {
age: number;
}
const user: Person = { name: "jth", age: 25 };
위 예제처럼 속성을 확장하는 것을 선언 병합 이라고 한다.
병합은 선언처럼 일반적인 코드이기 때문에 언제든지 가능하다. 프로퍼티가 추가되는 것을 원하지 않는다면 인터페이스 대신 타입을 사용해야 된다.
타입 별칭. 둘 다 사용할 수 있다면 일관성과 보강의 관점에서 고려하자.타입에서도 같은 코드를 반복하지 말라는 DRY(don't repear yourself) 원칙을 지키자.
interface Point2D{
x: number;
y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }
extendsinterface Person {
name: string;
}
interface User extends Person {
age: number;
}
type PersonWithAge = Person & { age: number };
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'>; // 제너릭 타입
매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식이다. 이 패턴은 표준 라이브러리에서도 찾을 수 있으며
Pick이라 한다.type Pick<T,K> = { [k in K]: T[k] };
다음처럼 태그된 유니온에서 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'
Action 유니온에 타입을 더 추가하면 ActionType은 자동적으로 그 타입을 포함한다. ActionType은 Pick을 사용하여 얻게 되는, type 속성을 가지는 인터페이스와는 다르다.
type ActionRec = Pick<Action, 'type'>; // {type: "save":"load"}
다음은 인스턴스가 생성되고 난 다음 프로퍼티가 업데이트 되는 클래스를 정의하는 경우다.
이 때 업데이트시 대부분의 타입들이 선택적 필드가 된다.
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'
매핑된 타입([k in keyof Options])은 순회하며 k 값에 해당하는 속성이 있는지 찾고 ?는 각 속성을 선택적으로 만드는데 이 패턴도 표준 라이브러리에 Partial으로 포함되어 있다.
class UIWidget {
constructor (init: Options) { /* */ }
update(options: Partial<Options>) { /* */ }
}
const INIT_OPTIONS = {
width: 654,
height: 480,
color: '#00FF00',
}
type Options = typeof INIT_OPTIONS;
/**
* 다음과 동일하다.
* interface Options{
* width: number;
* height: number;
* color: string;
* }
* /
여기서 사용된 typeof 는 런타임 연산자가 아니라 타입스크립트 단계에서 연산되어 강력한 타입 표현이 가능하다.
값으로부터 타입을 만들어 낼 때는
선언 순서에 주의해야한다. 타입 정의를 먼저 하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다. 그렇게 해야 타입이 더 명확해지고 예상하기 어려운 타입 변동을 방지할 수 있다.
ReturnType 제너릭이 있다.function getUserInfo(userId: string){
return{
userId,
name,
age,
height,
};
type UserInfo = ReturnType<typeof getUserInfo>;
DRY 원칙을 타입에도 최대한 적용하자!keyof, typeof, 인덱싱, 매핑된 타입)extends 사용타입스크립트에서는 타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다.
type Rocket = {[property: string]: string}
오타 잡기가 불가능){}도 유요한 Rocket 타입이 된다.인덱스 시그니처는
동적 데이터를 표현할 때 사용한다. 그 외에는 부정확하므로 더 나은 방법을 찾는 것이 좋다!
만약 데이터에 A,B,C,D 가 키로 있지만 얼마나 많이 있는지 모르는 경우 인덱스 필드 대신에 선택적 필드 로 모델링하는 것이 좋다.
interface Row = {
a: number;
b?: number;
c?: number;
d?: number;
}
Record는 키 타입에 유연성을 제공하는 제너릭 타입이다.type Vec3D = Record<'x' | 'y' | 'z', number>;
/**
Type Vec3D {
x: number;
y: number;
z: number;
}
*/
type Vec3D = {[k in 'x' | 'y' | 'z']: number};
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string: number}; //조건부 타입
undefined를 추가할 수도 있다.symbol, string만 가능하다. 값은 어떤 것이든 될 수 있음숫자는 키로 사용할 수 없기 때문에 만약 속성 이름으로 숫자를 사용하려 하면 자바스크립트 런타임은 문자열로 변환한다.
{ 1 : 2, 3 : 4}
{ '1' : 2, '3', : 4}
const x = [1, 2, 3];
Object.keys(x) // ['0', '1', '2']
타입스크립트는 이러한 혼란을 잡기위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식한다. 런타임에는 문자열 키로 인식하므로 완전히 가상이라고 할 수 있지만, 타입 체크 시점에 오류를 잡을 수 있어 유용하다.
length 프로퍼티를 갖고 배열과 유사한 반복 가능한 객체로 동작한다.... 이나 Array.from()을 사용해서 배열로 바꿔줘야 한다.const arr = [1, 2, 3]; // 배열 -> 숫자로 인덱스할 항목을 지정하는 경우 배열을 사용하는것이 바람직하다.
const arrLike = { // 유사 배열 객체. 키는 여전히 문자열이다.
'0': 1,
'1': 2,
'2': 3,
length: 3,
};
객체이기 때문에 키는 숫자가 아니라 문자열이다. 인덱스 시그니처로 사용된 number타입은 버그를 잡기 위한 순수 타입스크립트 코드이다.어떤 배열에 readonly 접근 제어자를 사용하면 다음과 같은 특징을 가지게 된다.
number[]는 readonly number[] 보다 기능이 많기 때문에,
readonly number[]의 서브타입이 된다. 따라서 변경 가능한 배열을 readonly 배열에 할당할 수 있다. 하지만 그 반대는 불가능하다.
매개변수를 readonly로 선언하면 다음과 같은 일이 생긴다.
readonly 는 객체를 변경할 수 없다는 뜻이지, 변수에 재할당이 불가능하다는 의미가 아니다.
let arr: readonly number[] = [1, 2, 3];
arr.push(4); // 'readonly number[]' 형식에 'push' 속성이 없습니다.
arr = [1, 2, 3, 4]; // 정상
만약 객체의 프로퍼티로 readonly 배열이 있더라도 객체 자체는 readonly는 아니다.
const dates: readonly Date[] = [new Date()]; // readonly 배열
dates.push(new Date()); // readonly Date[] 형식에 push 속성이 없습니다.
dates[0].setFullYear(2022); // 정상 -> Date 객체 자체는 readonly가 아니다.
Readonly 제너릭도 이와 유사하게 얕게 동작한다.
interface Outer {
inner: {
x: number;
}
}
const o: Readonly<Outer> = { inner: { x: 0 } };
o.inner = { x: 1 }; // 오류
o.inner.x = 1; // 정상
type T = Readonly<Outer>;
// Type T = {
// readonly inner: {
// x: number;
// };
// }
깊은 readonly 타입을 사용하고 싶다면
ts-essentials 라이브러리의 DeepReadonly 제너릭을 사용하면 된다.
readonly로 선언하자. 인터페이스를 명확하게 하며, 매개변수가 변경되는 것을 방지한다.readonly를 사용하면 변경하면서 발생하는 오류 방지 및 변경이 발생하는 코드도 쉽게 찾을 수 있다.산덤도를 그리기위한 UI 컴포넌트를 작성하는 경우에 디스플레이와 동작을 제어하기 위한 몇 가지 타입 속성이 포함된 예를 들어보자.
interface ScatterProps {
// data
xs: number[];
xs: number[];
// display
xRange: [number, number];
yRange: [number, number];
color: string;
// events
onClick: (x: number, y: number, index: number) => void;
}
필요할 때만 차트를 다시 그려야 하는 경우, 즉 데이터나 디스플레이 속성이 변경되면 다시 그리지만 이벤트 핸들러가 변경되면 다시 그릴 필요가 없다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
let k: keyof Scatterprops;
for(k in oldProps){
if(oldProps[k] !== newProps[k]){
if(k !== 'onClick')
return true;
}
}
return false;
}
위 예제에서 만약 새로운 속성이 추가되면 shouldUpdate 함수는 값이 변경될 때마다 차트를 다시 그릴 것이다.
이 접근법을 이용하면 차트가 정확하지만 너무 자주 그려질 가능성이 있다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps){
return(
oldProps.xs !== newProps.xs ||
oldProps.ys !== newProps.ys ||
oldProps.xRange !== newProps.xRange ||
oldProps.yRange !== newProps.yRange ||
oldProps.color !== newProps.color ||
);
}
차트를 불필요하게 다시 그리는 단점은 해결했지만 실제로 차트를 다시 그려야 할 경우에 누락될 수 있다.
shouldUpdate 함수는 ScatterProps 타입에 의존적이다. 따라서 둘의 관계를 연결시켜 동기화 해주어야 한다. 그래야 ScatterProps 타입이 업데이트 되어 변경되었을 때에 타입스크립트가 타입체크를 통해 shouldUpdate 또한 업데이트가 필요하다고 알릴 수 있다.
매핑된 타입과 객체를 사용해서 동기화 할 수 있다.
const REQUIRES_UPDATE: {[k in keyof ScatterProps]: boolean} {
// 매핑된 타입으로 REQUIRES_UPDATE 가 ScatterProps과 동일한 속성을 가져야 한다는 정보 제공
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;
}
}
매핑된 타입은 한 객체가 또 다른 객체와 정확히 같은 속성을 가지게 할 때 이상적이다. 위 예제처럼 매핑된 타입을 사용해 타입스크립트가 코드에 제약을 강제하도록 할 수 있다.