아이템15~아이템16까지 정리
- 아이템 15 : 동적 데이터에 인덱스 시그니처 사용하기
- 아이템 16 : number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기
인덱스 시그니처
- 타입스크립트에서는 타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다.
type Rocket = {[property: string]: string}; // 인덱스 시그니처
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 KN',
};
{[property: string]: string};
부분이 인덱스 시그니처 세 가지 의미를 가진다.- 키의 이름 : 키의 위치를 표시하는 용도.
- 키의 타입 : string, number, symbole을 사용할 수 있으며 보통은 string 사용
- 값의 타입 : 어떤 것이든 될 수 있음
인덱스 시그니처의 단점
- 잘못된 키를 포함해 모든 키를 허용한다
- ex ) name 대신 Name (O)
- 특정 키가 필요하지 않다.
- ex ) {}도 Rocket 타입 (O)
- 키마다 다른 타입을 가질 수 없다.
- ex ) thrust가 number여야 한다면 사용하지 못 한다.
- 타입스크립트의 언어 서비스를 사용할 수 없다.
- 자동 완성 등
인덱스 시그니처의 사용처
- 타입스크립트에서 제공하는 언어 서비스를 사용하기 위해서는 Rocket을 interface로 정의한다.
- 인덱스 시그니처는 동적 데이터를 표현할 때 사용한다.
- CSV 파일처럼 헤더 행(row)에 열(column)이 있고 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶을 때 사용할 수 있다.
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;
});
}
- 선언해둔 열이 실제로 일치한다는 보장이 없으니, 안전을 위해 값 타입에 undefined를 추가할 수 있다.
- 단, undefined를 체크하는 작업이 추가되기 때문에 번거로울 수 있다.
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;
});
}
interface ProductRow {
productId: string;
name: string;
price: string;
}
declare let csvData: string;
const products = parseCSV(csvData) as unknown as ProductRow[];
function safeParseCSV(
input: string
): {[columnName: string]: string | undefined}[] {
return parseCSV(input);
}
const rows = parseCSV(csvData);
const prices: {[produt: string]: number} = {};
for (const row of rows) {
prices[row.productId] = Number(row.price);
}
const safeRows = safeParseCSV(csvData);
for (const row of safeRows) {
prices[row.productId] = Number(row.price);
// ~~~~~~~~~~~~~ 'undefined' 형식은 인덱스 형식으로 사용할 수 없습니다.
연관 배열의 경우 인덱스 시그니처 대신 Map 타입 사용
- Map 타입은 프로토타입 체인과 관련된 문제를 해결하는데 유용하다. → 아이템 58 참고
- 어떤 타입에 가능한 필드가 제한되어 있는 경우 인덱스 시그니처를 사용하지 않는다.
- 선택적 필드, 또는 유니온 타입으로 모델링하면 된다.
interface Row1 { [column: string]: number } // 너무 광범위하다.
interface Row2 { a: number; b?: number; c?: number; d?: number } // 최선이다.
type Row3 =
| { a: number; }
| { a: number; b: number; }
| { a: number; b: number; c: number; }
| { a: number; b: number; c: number; d: number }; // 가장 정확하지만 사용하기 번거롭다.
string 타입이 광범위하여 인덱스 시그니처를 사용하기 문제가 있다면?
Record 사용
- Record는 키 타입에 유연성을 제공하는 제너릭 타입이다.
- 특히 string의 부분 집합을 사용할 수 있다.
type Vec3D = Record<'x' | 'y' | 'z', number>;
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
매핑된 타입 사용
- 매핑된 타입은 키마다 별도의 타입을 사용하게 해준다.
type Vec3D = {[k in 'x' | 'y' | 'z']: number};
// Same as above
type ABC = {[k in 'a' | 'b' | 'c']: k extends 'b' ? string : number};
// Type ABC = {
// a: number;
// b: string;
// c: number;
// }
가능하다면 인덱스 시그니처보다 인터페이스, Record, 매핑된 타입 같은 정확한 타입을 사용하는 것이 좋다.
number 인덱스 시그니처
- 자바스크립트에서 키는 보통 문자열이다. (ES2015 이후 심벌이 들어갈 수 있다.)
- 속성 이름으로 숫자를 사용하려고 하면 자바스크립트의 런타임은 문자열로 반환한다.
- 인덱스 시그니처를 number로 사용하는 건 순수 타입스크립트 코드이다.
- 런타임에서는 문자열로 변환되지만 타입 체크 시점에 오류를 잡을 수 있다.
- Object.keys와 같은 구문은 key의 타입을 string으로 반환한다.
- 인덱스 시그니처의 타입이 중요하다면 number를 사용하기 보다 Array나 튜플, ArrayLike 같은 타입을 사용하는 것이 좋다.
Array.prototype.forEach
const xs = [1, 2, 3];
xs.forEach((x, i) => {
i; // Type is number
x; // Type is number
});
타입이 불확실하다면 대부분의 브라우저와 자바스크립트 엔진에서 for-in 루프는 for-of, C 스타일 for 루프(;;)보다 몇 배 느리다.
ArrayLike
- 길이를 가지는, 배열과 비슷한 형태의 튜플을 사용하고 싶을 때 타입스크립트의 ArrayLike를 사용한다.
const xs = [1, 2, 3];
function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
if (i < xs.length) {
return xs[i];
}
throw new Error(`Attempt to access ${i} which is past end of array.`)
}
출처
- 이펙티브 타입스크립트