Frontend/Typescript

아이템 15. 동적 데이터에 인덱스 시그니처 사용하기

rachel_13 2023. 4. 22. 23:02

https://develub.kr/shop_view/?idx=26

타입스크립트에서는 타입에 '인덱스 시그니처'를 사용해서 유연하게 매핑을 표현할 수 있다.

예를 들어보자. 아래 객체는 키(key)값 만 다를 뿐 키와 쌍을 이루고 있는 값(value) 의 타입은 모두 'string'으로 동일하다.

const rocket = {
   name: 'Falcon 9'
   variant: 'Block 5'
   thrust: '7,607 kN',
};

따라서 객체의 타입을 다음과 같이 정의할 수 있다.

type RocketType = {[property: string]:string};

const rocket:RocketType = {..} //정상

  [property: string]: string   

이 형태가 인덱스 시그니처이며, 다음과 같은 특징을 가진다.

 

1. key의 이름 : 키의 위치만 표시하는 용도. 타입 체커에서는 사용 X

2. key의 타입 : string | number | symbol (보통은 string을 가장 많이 사용)

3. value의 타입 : 어느 타입이든 가능하다.

 

(단점)

  1. 잘못된 키를 포함해도 오류가 나지 않는다. 

  ex) name 대신 Name을 사용해도 오류로 인식하지 않는다.

  2. 특정 키가 필요하지 않다. 

  ex) { } 빈 객체도 RocketType으로 간주된다.

  3. 키마다 다른 타입을 가질 수 없다.

  ex) thrust에 string | number 타입을 넣고 싶은데, 그럴 수가 없다.

  4.  객체의 키 값 입력시 자동완성이 되지 않는다.

  (왜냐하면, key는 무엇이든 가능하기 때문)

 

따라서 인덱스 시그니처와 인터페이스를 적절하게 사용해야 한다.

위의 예에서는 인터페이스를 사용해서 타입을 선언해주어야 자동완성, 정의로 이동, 이름 바꾸기 등이 가능해진다.

interface RocketType {
   name: string;
   variant: string;
   thrust_KN: number;
}

const falconHeavy: RocketType = {
   name: 'Falcon Heavy',
   variant: 'v1',
   thrust_KN: 15_200
};

그렇다면 인덱스 시그니처는 어느 상황에서 사용해야 할까?

 

1. 동적 데이터를 표현할 때 사용한다.

즉, 특정한 상황에서 명시적으로 타입을 알고 있는 경우가 아닐 때 사용해야 한다.

런타임때까지 객체의 속성을 확정할 수 없는 경우에 사용한다.

 

ex) CSV 파일 - 행, 렬 데이터 매핑

function parseCSV(input: string):{[columName: string]: string}[] {
    const lines = input.split('\n');
    const [header, ...rows] = lines;
    console.log(header);
    console.log(rows);
    const headerColumns = header.split(',');
    return rows.map(rowStr => {
        const row: {[columnName: string]:string} = {};
        rowStr.split(',').forEach((cell, i) => {
            row[headerColumns[i]]= cell;
        });
        return row;
    })
}

 

어떤 column이 들어올지 알고 있다면, 선언해 둔 타입을 사용하는 것이 좋다. (인덱스 시그니처 보다는)

interface ProductRow {
   productId: string;
   name: string;
   price: string;
}

//특정한 상황에서 사용시 미리 선언해 둔 타입으로 단언하는 방법
declare let csvData: string;
const products = parseCSV(csvData) as unknown as ProductRowp[];

 

2. 안전한 접근을 위해 인덱스 시그니처 값 타입에 undefined를 추가하는 것을 고려해야 한다.

위의 예시에서 미리 선언해 둔 타입이 런타임에 실제로 일치한다는 보장이 없다. 

이럴 때는 undefined를 추가할 수 있는데, 별도의 체크를 추가해야 하기 떄문에 번거로울 수 있다.

function safeParseCSV(input:string): {[columName:string]:string | undefined}[] {
    return parseCSV(input);
}

 

3. 어떤 타입에 가능한 필드가 제한되어 있는 경우 → 인덱스 시그니처를 사용하지 말자

예를 들어 동일한 키가 있지만 타입의 범위가 너무 광범위 하거나, 예측이 잘 되지 않는 경우에는 선택적 필드유니온 타입을 사용함으로써 이를 해소할 수 있다.

예시)

/**
 * 1) Row1은 너무 광범위하다.
 */
interface Row1 {[column: string]: string}

/**
 * 2) Row2는 최선의 방법, a,b,c,d가 다 들어오는 것이 보장되지 않는다.
 */
interface Row2 {a: number; b?:number; c?:number; d?:number};

/**
 * 3) Row3는 가장 명확하게 타입을 명시한 방법이지만 번거롭다.
 */
type Row3 = 
| {a: number}
| {a: number; b: number;}
| {a: number; b: number; c: number;}
| {a: number; b: number; c: number; d: number;}

 

 

1) Record 사용하기

Record : 키 타입에 유연성을 제공하는 제너릭 타입

- 사용방법

type Record<K extends string | number | symbol, T> = { [P in K]: T};

- 사용예시

type CatInfo = {
    age: number;
    breed: string;
}

type CatName = "Brown" | "Boris" | "Sabong";

const cats: Record<CatName, CatInfo> = {
    Brown: {age: 10, breed: "Persian"},
    Boris: {age: 4, breed: "Maine Coon"},
    Sabong: {age: 7, breed: "British Shorthair"},
}

 

2) 매핑된 타입 사용하기

키(key)마다 별도의 타입을 사용하게 해준다.

type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number };

/**
 * Type ABC = {
 *  a: number;
 *  b: string;
 *  c: number;
 * }
 */