동적 데이터에 인덱스 시그니처 사용하기
자바스크립트 객체는 문자열 키를 타입의 값에 관계없이 매핑한다.
타입스크립트에서는 타입에 '인덱스 시그니처'를 명시하여 유연하게 매핑을 표현할 수 있다.
type Rocket = {[property: string]: string}
const rocket: Rocket = {
name: 'Falcon',
variant: 'v1.0',
thrust: '4940'
}; //정상
property: string이 인덱스 시그니처이며 다음 세 가지 의미를 담게 된다.
- 키의 이름: 키의 위치만 표시하는 용도이다. 타입 체커에서는 사용하지 않기 때문에 참고 정보로만 생각해도 된다.
- 키의 타입: string이나 number 또는 symbol의 조합이어야 하지만 보통은 string을 사용한다.
- 값의 타입: 어떤 것이든 될 수 있다.
이렇게 타입 체크가 수행되면 네 가지 단점이 드러난다.
- 잘못된 키를 포함해 모든 키를 허용한다
- 특정 키가 필요하지 않다
- 키마다 다른 타입을 가질 수 없다
- 타입스크립트 언어 서비스의 도움을 받을 수 없다
인덱스 시그니처는 동적 데이터를 표현할 때 사용한다. 예를 들어 CSV 파일처럼 헤더 행(row)에 열(col) 이름이 있고 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우이다.
function parseCSV(input: string): {[columnName: string]: string}[] {
const linees = input.split('\n');
const [header, ...rows] = linees;
const headerColumns = header.split(',');
return rows.map(rowStr => {
const row: {[columnName: string]: string} = {};
rowStr.split(',').forEach((cell, i) => {
row[headerColumns[i]] = cell;
});
return row;
});
}
일반적인 상황에서 열 이름이 무엇인지 미리 알 방법은 없다. 이럴 때 인덱스 시그니처를 사용한다. 반면에 열 이름을 알고 있는 특정한 상황에 parseCSV가 사용된다면 미리 선언해 둔 타입으로 단언문을 사용한다.
interface ProductRow {
productId: string;
name: string;
price: string;
}
declare let csvData: string;
const products = parseCSV(csvData) as unknown as ProductRow[];
선언해 둔 열들이 런타임에 실제로 일치한다는 보장이 없다. 이 부분이 걱정된다면 값 타입에 undefined를 추가할 수 있다.
function safeParseCSV(
input: string
): {[columnName: string]: string | undefined} [] {
return parseCSV(input)
}
이제 모든 열에 undefined 여부를 체크해야 한다.
const rows = parseCSV(csvData);
const prices: {[product: 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 타입을 사용하는 것을 고려할 수 있다. 이는 프로토타입 체인과 관련된 유명한 문제를 우회한다.
어떤 타입에 가능한 필드가 제한되어 있는 경우라면 인덱스 시그니처로 모델링 하지 말아야 한다. 예를 들어 데이터에 A,B,C,D 같은 키가 있지만 얼마나 많이 있는지 모른다면 선택적 필드 또는 유니온 타입으로 모델링하면 된다.
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};
number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 사용하기
자바스크립트는 이상하게 동작하기로 유명한 언어이다. 그 중 가장 악명 높은 것은 암시적 타입 강제와 관련된 부분이다.
암시적 타입 강제와 관련된 문제는 대부분 === 와 !== 를 사용해서 해결이 가능하다.
자바스크립트에서 객체란 키/값 쌍의 모음이다. 키는 보통 문자열이다. 그리고 값은 어떤 것이든 될 수 있다.
파이썬이나 자바에서 볼 수 있는 '해시 가능' 객체라는 표현이 자바스크립트에는 없다. 만약 더 복잡한 객체를 키로 사용하려고 하면, toString 메서드가 호출되어 객체가 문자열로 변환된다.
특히 숫자는 키로 사용할 수 없다. 만약 속성 이름으로 숫자를 사용하려고 하면, 자바스크립트의 런타임은 문자열로 변환한다.
{1: 2, 3: 4} // {'1': 2, '3': 4}
배열은 분명한 객체이다.
typeof [] // 'object'
그러니 숫자 인덱스를 사용하는 것이 당연하다. 하지만 인덱스들은 사실 문자열로 변환되어 사용된다.
문자열 키를 사용해도 배열의 요소에 접근하는 것이 가능하다.
x = [1,2,3]
Object.keys(x) // ['0', '1', '2']
타입스크립트는 이러한 혼란을 바로잡기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식한다.
interface Array<T> {
// ...
[n: number]: T;
}
어떤 길이를 가지는 배열과 비슷한 형태의 튜플을 사용하고 싶다면 타입스크립트에 있는 ArrayLike 타입을 사용한다.
function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
if (i < xs.length) {
return xs[i];
}
throw new Error(`배열의 끝을 지나서 ${i}를 접근하려고 했습니다.`)
}
이 예제는 길이와 숫자 인덱스 시그니처만 있다. 이런 경우가 실제로는 드물지만 필요하다면 ArrayLike를 사용해야 한다. 그러나 ArrayLike를 사용하더라도 키는 여전히 문자열이라는 것을 잊지 말자.
'Web > Typescript' 카테고리의 다른 글
effective Typescript (7) (0) | 2025.03.19 |
---|---|
effective Typescript (6) (0) | 2025.03.18 |
effective Typescript (4) (0) | 2025.03.16 |
effective Typescript (3) (0) | 2025.03.14 |
effective Typescript (2) (0) | 2025.03.13 |