일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Framer motion
- 테오의 스프린트
- 타입스크립트
- TS
- 시스템디자인
- 개발자를 위한 글쓰기 가이드
- CSS
- react
- framer
- ASP.NET
- framer-motion
- 이펙티브타입스크립트
- 캐나다개발자
- VS Code
- typescript
- useState
- JUNCTION2023
- Semantic Versioning
- 알고리즘
- SemVer
- 캐나다취준
- 코드트리
- 글또 10기
- CSS방법론
- React-Router-Dom
- JSBridge
- 개발자 원칙
- Effective Typescript
- 회고
- 글또
- Today
- Total
큰 꿈은 파편이 크다!!⚡️
이펙티브 타입스크립트 🦅 2장 (2) 본문
2장 (2)를 작성하며..
사실 2장에 들어가면서부터 꽤 어려운 개념들이 나오는건지 책을 읽어나가기가 쉽지않았다 ㅎㅎ 개념도 그렇고 한글 문장 자체도 쉽게 이해되지는 않았다. 무엇보다 몇 주만에 다음 장을 다시 읽다보니 분명히 이해했고, 너무 감명깊게 읽었던 앞 장의 내용들이 기억이 나지 않았다.. 역시 읽을때 이해하는 것만으로는 머릿속에 들어오지 않나보다.
11. 잉여 속성 체크의 한계 인지하기
구조적 타이핑 관점에서 보면 아래 코드는 오류가 나타나지 않아야 한다. 그럼에도 불구하고 나타나는 이유는 잉여 속성 체크가 수행되었기 때문이다. 타입이 명시된 변수에 객체 리터럴을 할당할 때, TS는 해당 타입의 속성이 있는지, 그리고 그 외의 속성은 없는지 확인한다.
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r: Room = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
// and 'elephant' does not exist in type 'Room'
};
TS는 의도와 다르게 작성된 코드도 찾으려 한다. 그렇기에 잉여 속성 체크를 이용하면 객체 리터럴에 알 수 없는 속성을 허용하지 않을 수 있다.
아래 경우에는 obj
의 타입이 { numDoors: number; ceilingHeightFt: number; elephant: string; }
으로 추론되며, obj
타입은 Room
타입의 부분집합을 포함하므로 Room
에 할당 가능하며 타입 체커도 통과한다. 왜일까? const obj
는 타입 구문이 없는 임시 변수로, 객체 리터럴이지만 const r: Room = obj
의 obj
는 객체 리터럴이 아니므로 잉여 속성 체크가 적용되지 않기 때문에 오류가 없다.
const obj = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
};
const r: Room = obj; // OK
잉여 속성 체크는 타입 단언문을 사용할 때에도 적용되지 않는다. 타입 단언문보다 선언문을 사용해야 하는 이유임을 다시한번 강조한다.
const obj = {
numDoors: 1,
ceilingHeightFt: 10,
elephant: 'present',
} as Room; //정상
잉여 속성 체크를 원하지 않는다면 인덱스 시그니처를 사용해서 TS가 추가적인 속성을 예상하도록 할 수 있다.
interface Options {
darkMode?: boolean;
[otherOptions: string]: unknown;
}
const o: Options = { darkmode: true }; // OK
12. 함수 표현식에 타입 적용하기
JS, TS에서는 함수 문장statement과 함수 표현식expression을 다르게 인식한다
function rollDice1(sides: number): number { /* COMPRESS */ return 0; /* END */ } // Statement
const rollDice2 = function(sides: number): number { /* COMPRESS */ return 0; /* END */ }; // Expression
const rollDice3 = (sides: number): number => { /* COMPRESS */ return 0; /* END */ }; // Also expression
TS에서는 함수 표현식을 사용하자. 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있는 장점이 있기 때문이다. 아래 코드는 반복되는 함수 시그니처를 하나의 함수 타입으로 통합했다.
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;
함수의 매개변수에 타입 선언을 하는 것보다 함수 표현식 전체 타입을 정의하는 것은 안전하고 간결한 코드를 만든다. 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성하거나, 동일한 타입 시그니처를 가지는 여러 개의 함수를 작성할 때는 함수 전체의 타입 선언을 적용하자. 다른 함수의 시그니처를 참조하려면 typeof fn
을 사용한다.
13. 타입과 인터페이스의 차이점 알기
TS에서 명명된 타입named type을 정의할 때는 type 또는 interface 를 사용하는 두가지 방법이 있다. 차이를 알고 상황에 따라 일관성을 유지하기 위해 차이점을 살펴보자.
(이하 인터페이스인 경우 접두사로 I를, 타입인 경우 T를 붙인다. 다만 실제 코드에서 인터페이스 접두사로 I를 붙이는 것은 C#의 관례이므로 현재는 지양해야 할 스타일로 여겨진다)
type TState = {
name: string;
capital: string;
}
interface IState {
name: string;
capital: string;
}
공통점
- 추가 속성을 할당한다면 동일한 오류가 발생한다.
- 인덱스 시그니처를 사용할 수 있다.
- 함수 타입을 인터페이스나 타입으로 정의할 수 있다.
type TFnWithProperties = {
(x: number): number;
prop: string;
}
interface IFnWithProperties {
(x: number): number;
prop: string;
}
- 제너릭이 가능하다.
- 인터페이스는 타입을 확장할 수 있고, 타입은 인터페이스를 확장할 수 있다.
- 단, 인터페이스는 유니온 타입같은 복잡한 타입은 확장할 수 없다.
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 Input = { /* ... */ };
type Output = { /* ... */ };
//유니온 타입 확장
interface VariableMap {
[name: string]: Input | Output;
}
//유니온 타입에 속성을 붙여 타입을 만드는 경우
type NamedVariable = (Input | Output) & { name: string };
- 타입으로 튜플과 배열 타입을 더 간결하게 표현할 수 있다.
- 인터페이스로 튜플을 비슷하게 구현할 수 있으나, 튜플에서 사용할 수 있는 메서드들을 사용할 수 없다.
- 인터페이스는 보강augment이 가능하다.
- 타입은 기존 타입에 보강이 없는 경우에만 사용해야 한다.
- 예제. 선언 병합 declaration merging
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000
}; // OK
언제 뭘 써야할까?
- 타입이 복잡한 경우 ⇒
type
- 간단한 타입이라면 ⇒ 일관성과 보강의 관점에서 고려
- API 타입 선언 ⇒
interface
- api가 변경될 때 사용자가 인터페이스로 새로운 필드를 병합할 수 있다
- 프로젝트 내부 타입에 선언 병합이 발생하면 잘못된 설계임. 방지 위해 ⇒
type
- 일반적으로는 type이 interface보다 쓰임새가 많긴 하다
14. 타입 연산과 제너릭 사용으로 반복 줄이기
DRY(Don’t repeat yourself)원칙을 타입에서도 잊지 말자. 타입에서 반복을 줄이는 가장 간단한 방법은 이름을 붙이는 것이다.
//👎
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 {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
이미 존재하는 타입을 확장하는 경우, 인터섹션(&) 연산자를 쓸 수도 있다.
type PersonWithBirthDate = Person & { birth: Date };
Generic - Pick
애플리케이션의 전체 상태를 표현하는 State
타입과 TopNavState
가 있을 때, State
의 부분 집합으로 TopNavState
를 정의하는 것이 전체 앱의 상태를 하나의 인터페이스로 유지할 수 있게 한다.
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
}
State
를 인덱싱하여 속성의 타입에서 중복을 제거한다.
type TopNavState = {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
};
매핑된 타입을 이용하여 중복을 한번 더 제거한다. 매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식으로, 표준 라이브러리에서 Pick
이라는 제너릭 타입으로 대체할 수 있다.
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
//🤌 type Pick<T,K> = { [k in K]: T[k] };
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
태그된 유니온에서도 중복이 발생할 수 있다. Action
유니온을 인덱싱하면 타입 반복 없이 ActionType
을 정의할 수 있으며, Pick
을 사용하여 얻는 인터페이스와는 다르다.
interface SaveAction {
type: 'save';
// ...
}
interface LoadAction {
type: 'load';
// ...
}
type Action = SaveAction | LoadAction;
//👎 Repeated types!
type ActionType = 'save' | 'load';
//👍 Type is "save" | "load"
type ActionType = Action['type'];
//Pick: {type: "save" | "load" }
type ActionRec = Pick<Action, 'type'>;
Generic - Partial
한번 생성한 뒤 업데이트되는 클래스를 정의한다면, update
메서드 매개변수의 타입은 생성자와 동일한 매개변수이면서, 타입 대부분이 선택적 필드가 된다.
interface Options {
width: number;
height: number;
color: string;
label: string;
}
interface OptionsUpdate {
width?: number;
height?: number;
color?: string;
label?: string;
}
class UIWidget {
constructor(init: Options) { /* ... */ }
update(options: OptionsUpdate) { /* ... */ }
}
매핑된 타입과 keyof
를 사용하면 Options
로부터 OptionsUpdate
를 만들 수 있다. keyof
는 타입을 받아서 속성 타입의 유니온을 반환한다.
type OptionsUpdate = {[k in keyof Options]?: Options[k]};
//type OptionsKeys = keyof Options = "width" | "height" | "color" | "label"
매핑된 타입은 순회하며 ?
를 통해 각 속성을 선택적으로 만드는데, 이 패턴은 표준 라이브러리의 Partial
로 포함되어 있어 아래와 같이 사용할 수 있다.
class UIWidget {
constructor(init: Options) { /* ... */ }
update(options: Partial<Options>) { /* ... */ }
}
값의 형태에 해당하는 타입을 정의하고 싶을 때에는 typeof
를 사용한다. 이는 JS의 런타임 연산자가 아닌 TS단계의 연산이다. 단, 값으로부터 타입을 만들 때에는 타입 정의를 먼저 한 뒤 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다.
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
type Options = typeof INIT_OPTIONS;
Generic - ReturnType
함수나 메서드의 반환 값에 명명된 타입을 만들고 싶은 경우 조건부 타입이 필요한데, 표준 라이브러리에는 이에 맞는 ReturnType
제너릭이 존재한다. ReturnType
은 함수의 값인 getUserInfo
가 아니라, 함수의 타입인 typeof getUserInfo
에 적용되었다. 적용 대상이 타입인지 값인지 정확히 아는 것이 중요하다.
function getUserInfo(userId: string) {
// COMPRESS
const name = 'Bob';
const age = 12;
const height = 48;
const weight = 70;
const favoriteColor = 'blue';
// END
return {
userId,
name,
age,
height,
weight,
favoriteColor,
};
}
// Return type inferred as { userId: string; name: string; age: number, ... }
type UserInfo = ReturnType<typeof getUserInfo>;
함수에서 매개변수로 매핑할 수 있는 값을 제한하기 위해 타입 시스템을 사용하는 것처럼 제너릭 타입에서 매개변수를 제한할 수 있는 방법이 필요하다. extends
를 사용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있다.
아래 예제에서 K
는 T
타입과 무관하고 범위가 너무 넓다. 또한 K
는 인덱스로 사용될 수 있는 string | number | symbol
이 되어야 하며 범위를 더 좁힐 수도 있다. K
는 실제로 T
의 키의 부분 집합인 keyof T
가 되어야 한다.
타입이 값의 집합이라는 관점에서 extends
는 ‘확장’이 아닌 ‘부분 집합’이다.
type Pick<T, K> = {
[k in K]: T[k]
// ~ K 타입은 'string | number | symbol' 타입에 할당할 수 없다
}
type Pick<T, K extends keyof T> = {
[k in K]: T[k]
}
이를 통해 Pick
에 잘못된 키를 넣으면 오류가 발생하는 것을 확인할 수 있다.
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T];
type FirstLast = Pick<Name, 'first' | 'last'>; // OK
type FirstMiddle = Pick<Name, 'first' | 'middle'>;
// ~~~~~~~~~~~~~~~~~~
// Type '"middle"' is not assignable
// to type '"first" | "last"'
마무리
책을 읽고 학습한 내용을 정리하는 것도 의외로 어렵다. 읽으면서 정리하기 때문에 이해했다고 착각하기도 하고, 나도 모르게 그냥 책의 내용을 옮겨쓰는것처럼 되어버리는 느낌을.. 계속 경계해야겠다.
이번 글의 범위에서 인상깊게 배운 것은 Pick을 통해 생성/업데이트용 변수를 구분해서 사용할 수 있다는 점이다. 더더욱 인상깊은 건 I를 인터페이스 타입 앞에 붙이는 건 지양해야 하는 컨벤션이라는 것이다..🥹
'Web FE' 카테고리의 다른 글
리액트 앱 배포 후 Cache 문제 🧹 삽질기 (0) | 2023.02.26 |
---|---|
이펙티브 타입스크립트 🦅 2장 (3) (0) | 2022.10.08 |
이펙티브 타입스크립트 🦅 2장 (1) (0) | 2022.08.28 |
이펙티브 타입스크립트 🦅 1장 (0) | 2022.08.15 |
BEM: CSS 네이밍 방법론 (0) | 2022.08.06 |