[2장] 타입스크립트의 타입 시스템 (2 / 2)이펙티브 타입스크립트 2장의 아이템 12 - 18을 정리한 내용입니다.작성일 2024.02.14페이지가 생성된 시간 2024.11.16 14:27:45
0
아이템12
함수 표현식에 타입 적용하기
- 타입스크립트에서는 함수 표현식을 사용하는게 더 좋음.
- 함수 타입을 선언해 재사용할 수 있기 때문이다
- 동일한 타입의 시그니처를 가지는 여러 개의 함수를 작성할 때
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;
- 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성할 때
- 다른 함수의 시그니처를 참조하려면 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;
};
아이템13
타입과 인터페이스의 차이점
- 명명된 타입 정의 시 타입과 인터페이스 둘 다 사용이 가능
type TState = {
name: string;
capital: string;
};
interface IState {
name: string;
capital: string;
}
- 대부분의 경우에 둘 다 사용이 가능하지만 차이를 분명하게 알고 일관성있게 사용해야 함.
- 함수타입에 추가적인 속성이 있다면 타입별칭과 인터페이스는 별 차이 없음
// 일반적인 함수타입 선언
type TFn = (x: number) => string;
interface IFn {
(x: number): string;
}
// 추가적인 속성이 있을 때
type TFnWithProperties = {
(x: number): number;
prop: string;
};
interface IFnWithProperties {
(x: number): number;
prop: string;
}
- 인터페이스는 유니온, 인터섹션을 통한 확장이 안됨(extends 는 가능)
// 확장 예시
// 인터페이스는 extends 사용
interface IStateWithPop extends TState {
population: number;
}
// 타입은 인터섹션 사용
type TStateWithPop = IState & { population: number };
- implements 할 때는 타입별칭, 인터페이스 모두 가능
class StateT implements TState {
name: string = '';
capital: string = '';
}
class StateI implements IState {
name: string = '';
capital: string = '';
}
- 타입별칭은 매핑된 타입(xx in keyof XX), 조건부타입(삼항연산자) 가능
- 매핑된 타입은 다음 아이템(14)에서 다룸
T extends U ? : X : Y
- 타입별칭은 튜플과 배열타입 표현도 쉽다.
type A = [string, ...number[]];
const a: A = ['1', 1, 2, 3, 4, 5];
- 인터페이스는 보강이 가능하다. 같은 이름으로 선언하면 합쳐짐. (보강 == 선언병합 ??)
interface IState {
name: string;
capital: string;
}
interface IState {
population: number;
}
const wyoming: IState = {
name: 'Wyoming',
capital: 'Cheyenne',
population: 500_000,
};
같은 상황에서는 동일한 방법을 써서 일관성을 유지하는 것
이 중요
결론: 추가적인 보강이 필요할 것 같다면 인터페이스를 쓰자. 그 외에는 타입.. 이것이 정답은 아니지만 중요한 것은 아이템14
타입 연산과 제네릭 사용으로 반복 줄이기
- 타입간 매핑하는 방법을 알아야 타입 정의에서도 DRY의 장점을 적용 가능하다
- 타입을 확장하는 방법
- 인터페이스 extends
- 유니온, 인터섹션 타입
- 타입의 부분만 표현하는 방법: 매핑된 타입
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k];
};
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
- 태그된 유니온에서 중복 줄이기
interface SaveAction {
type: 'save';
}
interface LoadAction {
type: 'load';
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // type 반복(하드코딩?)
type ActionType = Action['type'];
- 매핑된 타입 활용 시 선택적 속성으로 변경 쉬움
interface Options {
width: number;
height: number;
color: string;
label: string;
}
interface OptionsUpdate {
width?: number;
height?: number;
color?: string;
label?: string;
}
type OptionsUpdate = { [k in keyof Options]?: Options[k] };
// 표준 라이브러리 Partial
type Partial<T> = { [k in keyof T]?: T[k] };
- 타입추론된 함수의 반환 값에 명명된 타입 만들기
function getUserInfo(userId: string) {
// ...
return { userId, name, age };
// 추론된 반환 타입: { userId: string; name: string; age: number }
}
type UserInfo = ReturnType<typeof getUserInfo>;
- 제네릭 타입은 타입을 위한
함수
와 같다.함수
는 코드에 대한 반복을 줄여줌 - 제네릭 타입에서 매개변수를 제한할 수 있는 방법이 필요하다.
- extends 를 통해 매개변수가 특정 타입을 확장한다고 하면 된다.
interface Name {
first: string;
last: string;
}
type DancingDuo<T extends Name> = [T, T];
// 오류나는 코드
const couple1: DancingDuo<Name> = [{ first: 'Fred' }, { first: 'Ginger' }];
// 실제 값이 Name타입의 상위집합임
아이템15
동적으로 인덱스 시그니처 활용하기
- 인덱스 시그니처 키의 타입은 string, number, symbol이 가능. 하지만 주로 string 사용
- 인덱스 시그니처의 단점
- 모든 키를 허용
- 빈 객체도 유요한 타입이 됨
- 키마다 다른 타입을 가질 수 없음
- 타입스크립트의 언어서비스(자동완성)를 사용할 수 없음 (어떠한 키도 모두 가능하기 때문)
- 그러면 언제 인덱스 시그니처를 사용하기에 적절한가
- 키의 이름이 무엇인지 알 필요가 없을 때
function parseCSV(input: string): { [columnName: string]: string }[] {
const lines = input.split('\n');
const [header, ...rows] = lines;
return rows.map((rowStr) => {
const row: { [columnName: string]: string } = {};
rowStr.split(',').forEach((cell, i) => {
row[header[i]] = cell;
});
return row;
});
}
- 연관 배열의 경우, 객체에 인덱스 시그니처를 사용하는 것 대신 Map타입의 사용을 고려할 수 있다. 이는 프로토타입 체인과 관련된 유명한 문제를 우회한다. 아이템 58 참고
- 아이템 58의 내용
- 연관 배열이란? key, value로 묶인 배열?
- 문자열이 주어지고 단어마다 쓰인 횟수를 저장하는 연관 배열이 필요하다고 가정
- 문자열에 'constructor' 같이 Object.prototype에 있는 단어가 들어가면 의도하지 않은 결과가 나올 수 있음.
- 따라서 이런 경우 Map을 사용하자(자바스크립트 관련 지식인듯)
function countWords(text: string) {
const counts = {[word: string]: string} = {};
for (const word of text.split(/[\s,.]+/)) {
counts[word] = 1 + (counts[word] || 0);
}
return counts;
}
console.log(countWords('Object have a constructor'));
// 실행결과
{
Objects: 1,
have: 1,
a: 1,
constructor: "1function Object() { [native code] }",
}
- 레코드와 매핑된 타입을 사용하면 인덱스 시그니처보다 더 정확하게 타입을 표현할 수 있음
type Vec3D = Record<'x' | 'y' | 'z', number>;
type Vec3D = { [k in 'x' | 'y' | 'z']: number };
type Vec3D = {
x: number;
y: number;
z: number;
};
// 매핑된 타입은 키마다 별도의 타입 지정 가능
type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number };
type ABC = {
a: number;
b: string;
c: number;
};
아이템16
number 인덱스 시그니처 대신 Array, 튜플, ArrayLike 사용하기
- number로 키의 타입을 정의해도 어차피 런타임에 자바스크립트는 키를 string으로 바꾼다. 이걸 꼭 알아두자
- for in 문은 for of 문보다 몇 배는 느리다. for in 문은 프로토타입 체인을 따라가게 될 수도 있으며, 모든 열거가능 속성을 반복하므로 불확실한 타입이 발생할 수 있다.
- Array 타입은 순서가 있는 요소들의 배열을 나타내는 반면, ArrayLike 타입은 순서가 있는 요소들의 배열과 유사하지만 배열이 아닌 타입을 나타낸다. ArrayLike는 배열 메서드를 사용할 수 없으므로, 배열과는 다른 점이 있음
결론: number 인덱스 시그니처 쓰지말고, Array, ArrayLike 쓰자
아이템17
변경 관련 오류를 방지하는 readonly
- readonly는 배열 또는 튜플 리터럴에만 붙일 수 있음을 알자.
- 자바스크립트에서 원본을 변경하는 메서드 등이 있음.
- 원본 변경이 싫다면 readonly를 붙여서 의도치 않은 변경을 막을 수 있다.
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;
}
- number[] 은 readonly number[] 보다 기능이 많기 때문에(확장되었기 때문에) readonly number[] 타입의 서브타입이다.
- number[] 가 readonly number[] 보다
변경 가능
이라는 기능이 더 있다고 이해했음
- number[] 가 readonly number[] 보다
- 따라서 readonly number[] 는 number[]에 할당할 수 없다
- 위 문장으로 이해해도 되지만, 변경될 수 있는 number[]에 변경 불가능한 readonly number[]를 할당하는 것은 당연히 불가능
- 함수의 매개변수를 readonly로 선언하면 호출하는 쪽에서 함수에 readonly 타입을 전달할 수 있다.
- readonly 불편하지만 인터페이스를 명확히 하고 타입 안전성을 높일 수 있다.
- readonly는 얕게 동작한다.
// 두 가지의 차이 이해하기
(readonly string[])[] // readonly 배열의 변경 가능한 배열
readonly string[][] // 변경 가능한 배열의 readonly 배열
interface Outer {
inner: {
x: number;
};
}
const o: Readonly<Outer> = { inner: { x: 0 } };
o.inner = { x: 1 };
// ~~~~ Cannot assign to 'inner' because it is a read-only property
o.inner.x = 1; // OK
- DeepReadonly 제네릭이 있음. 그걸 가져다 쓰자.
아이템18
매핑된 타입을 사용하여 값을 동기화하기
- 매핑된 타입을 사용해 타입스크립트가 코드에 제약을 강제하도록 할 수 있다.
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;
}
// 너무 렌더링이 많이 일어나는 문제
// 보수적, 실패에 닫힌 접근법
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;
}
// 렌더링이 누락되는 문제
// 실패에 열린 접근법
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
// (no check for onClick)
);
}
// 새로운 속성이 추가 될 때 변경을 강제하고 싶다...
// 아래는 매핑된 타입과 객체를 활용해 타입 체커가 동작하도록 수정한 코드!
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;
}