Item15 - 동적 데이터에 인덱스 시그니처 사용하기
자바스크립트 객체는 문자열 키를 타입의 값에 관계없이 매핑합니다. 타입스크립트에서는 타입에 "인덱스 시그니처"를 명시하여 유연하게 매핑을 표현할 수 있습니다.
type Rocket = { [property: string]: string }
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
thrust: '4,940 kN',
} // OK
[property: string]: string 이 인덱스 시그니처이며 세 가지 의미를 담고 있습니다.
- 키의 이름 : 키의 위치만 표시하는 용도. 타입 체커에서는 사용하지않기때문에 중요하지않음
- 키의 타입 : string 이나 number 또는 symbol의 조합이어야함. 보통은 string을 사용
- 값의 타임 : 어떤 것이든 값의 타입이 될 수 있음
하지만 이렇게 타입체크가 수행된다면 네 가지 단점이 있습니다.
- 잘못된 키를 포함해 모든 키를 허용한다. 위 코드를 예시로 하자면 name대신 Name으로 작성해도 유효한 타입이 된다.
- 특정 키가 필요하지 않다. {}도 유효한 Rocket 타입이 된다.
- 키마다 다른 타입을 가질 수 없다. thrust 는 string이 아니라 number 를 쓰고 싶을 수 있지만 불가능
- 타입스크립트의 자동완성 기능이 동작하지 않는다. 왜냐하면 키는 무엇이든지 가능(허용)하기때문
이와같은 경우 단점을 극복하기 위해선 인터페이스를 사용하면 됩니다.
interface Rocket {
name: string
variant: string
thrust_kN: number
}
const falconHeavy: Rocket = {
name: 'Falcon Heavy',
variant: 'v1',
thrust_kN: 15_200,
}
이제 thrust_kN 은 number 타입이며 타입스크립트는 모든 필수 필드가 존재하는지 확인합니다. 또한 자동완성기능도 작동 할 것입니다.
인덱스 시그니처는 동적 데이터를 표현할 때 사용합니다.
예를들어 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
})
}
0일반적인 상황에서 열 이름이 무엇인지 미리 알 방법은 없습니다. 이럴때 인덱스 시그니처를 사용합니다.
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[]
반면에 열 이름을 알고 있는 특정한 상황에서 parseCSV가 사용된다면 미리 선언해둔 타입을 단언문으로 사용합니다.
function safeParseCSV(input: string): { [columnName: string]: string | undefined }[] {
return parseCSV(input)
}
하지만 선언해 둔 열들이 런타임에서 실제로 일치한다는 보장이 없기때문에 undefined 를 추가할 수 있습니다.
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 }
// 가장 정확하지만 사용하기 번거롭다
어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링하지 말아야합니다. 예를 들어 데이터에 A,B,C,D 같은 키가 있지만 얼마나 많이 있는지 모른다면 선택적 필드 또는 유니온 타입으로 모델링 하면 됩니다.
string 타입이 너무 광범위해서 인덱스 시그니처를 사용하는데 문제가 있다면 두가지 방법이 있습니다.
첫번째 Record 를 사용하는 방법입니다. Record는 키 타입에 유연성을 제공하는 제네릭 타입입니다. 특히 string 의 부분집합에 사용할수있습니다.
type Vec3D = Record<'x' | 'y' | 'z', number>
// Type Vec3D = {
// x: number;
// y: number;
// z: number;
// }
const test:Vec3D={
x: 0,
y: 0,
z: 0
}
type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number }
// Type ABC = {
// a: number;
// b: string;
// c: number;
// }
// b인 key 가 있다면 string 타입으로 제한해줌
요약
- 런타임 때까지 객체의 속성을 알수 없을 경우에만 인덱스 시그니처를 사용하자.
- 안전한 접근을 위해 인덱스 시그니처 값에 undefined를 추가하는것을 고려하자
- 가능하다면 인터페이스,Record,인덱스 시그니처 보다 정확한 타입을 사용하는것이 좋다
'Typescript' 카테고리의 다른 글
Effective Typescript - 아이템17 (0) | 2023.04.12 |
---|---|
Effective Typescript - 아이템16 (0) | 2023.04.11 |
Effective Typescript - 타입 연산과 제네릭 사용으로 반복 줄이기 (0) | 2023.04.11 |
Effective Typescript - 타입과 인터페이스의 차이점 알기 (0) | 2023.04.09 |
Effective Typescript - 함수 표현식에 타입 적용하기 (0) | 2023.04.09 |