Typescript

[Typescript] 이펙티브 타입스크립트 2장 아이템15-16

sian han 2023. 3. 7. 09:24

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

타입스크립트에서는 타입에 인덱스 시그니처 를 명시해 매핑을 유연하게 표현한다.

type Rocket = {[property: string]: string};
const rocket: Rocket = {
    name: 'Falcon 9',
    variant: 'v1.0',
    thrust: '4,940 kN',
};

위 코드에서 인덱스 시그니처 [property: string]: string 는 다음 의미를 갖고 있다.

  • 키의 이름 : 키의 위치만 표시하는 용도
  • 키의 타입 : string | number | symbol
  • 값의 타입 : 어떤것이든 될 수 있음

 

▷ 위와 같이 타입 체크가 수행되었을 때의 단점

  • 잘못된 키를 포함해 모든 키를 허용한다
const rocket: Rocket = {
    Name: 'Falcon 9', //name 대신 Name으로 작성해도 유효하다
    variant: 'v1.0',
    thrust: '4,940 kN',
};

 

  • 특정키가 필요하지 않다
const rocket1: Rocket = {} //정상

 

  • 키마다 다른 타입을 가지는 것이 불가하다
const rocket: Rocket = {
    Name: 'Falcon 9',
    variant: 'v1.0',
    thrust: 4940,
};

 

  • 키는 무엇이든 가능하기 때문에 자동완성 기능이 동작하지 않는다

 

 

인덱스 시그니처를 인터페이스로 변경하면 단점들을 보완하고 타입스크립트에서 제공하는 자동완성, 정의로 이동, 이름바꾸기 등의 언어 서비스를 모두 사용할 수 있게 된다.

interface Rocket {
    name: string;
    variant: string;
    thrust_kN: number;
}

const falconHeavy: Rocket = {
    name: 'Falcon',
    variant: 'v1.0',
    thrust_kN: 15_200
}

 

 

 그렇다면 언제 인덱스 시그니처를 사용해야할까 ? 

 

동적 데이터를 표현할 때 사용한다. 동적 속성을 포함하는 객체를 다루는 경우, 타입스크립트는 해당 속성의 타입을 정적으로 파악할 수 없기 때문에 타입검사를 수행할 수 없다. 따라서 이러한 경우 인덱스 시그니처를 사용해서 해당 속성의 타입을 정의할 수 있다.

 

ex ) 데이터 행을 열 이름과 값으로 매핑하는 개체로 나타내고 싶은 경우

function parseCSV(input: string):{[columnName: string]: string}[]{
    const lines = input.split('\n'); //CSV 문자열 분할
    const [header, ...rows] = lines; // 헤더행 추출
    const headerColumns = header.split(','); // 헤더행의 각 열 이름 추출

    return rows.map(rowStr => { 
        const row: {[columnName: string]: string} = {}; //빈 객체 row
        rowStr.split(',').forEach((cell,i)=>{ 
            row[headerColumns[i]]=cell; //열이름과 값을 매핑해서 row 객체에 저장
        });
        return row; //rows 배열에 추가됨
    });
}

const inputData = `name,age,city
Alice,25,New York
Bob,30,London
Charlie,35,Paris`;

const result = parseCSV(inputData);

//[
//  { name: 'Alice', age: '25', city: 'New York' },
//  { name: 'Bob', age: '30', city: 'London' },
//  { name: 'Charlie', age: '35', city: 'Paris' }
//]

이와 같이 런타임때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용하도록 한다

 

타입에 가능한 필드가 제한되어 있는 경우에는 인덱스 시그니처로 모델링 하지 않는다

interface Row1 {[column: string]: number} //광범위하다

 

 

 

▶ 인덱스 시그니처의 대안

1. Record

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

  - string 의 부분집합을 사용할 수 있다

Record<K, T> // K : 속성 이름의 타입, T : 속성 값의 타입
type Vec3D = Record<'x' | 'y' | 'z', number>; //Vec3D 객체의 속성 이름은 x,y,z 이고, 값은 number 타입이다

 

 

2. 매핑된 타입

  - 키마다 별도의 타입을 사용하게 해준다

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

 


※ 아이템16 number 인덱스 시그니처보다는 Array, 튜플, ArrayLike 를 사용하기

 

자바스크립트의 객체 : 키, 값 쌍의 모음

  - 키는 보통 문자열이다

  - 값은 어떤것이든 될 수 있다

  - 해시가능 객체라는 표현이 없음 (파이썬, 자바 有)

    - 해시가능객체 ? 해시 함수를 사용해서 해시 테이블 같은 자료구조에서 Key 로 사용할 수 있는 객체이다. 해시 가능한 객체는 대표적으로 String, Integer, Double 등이 있다.

 

>{1: 2, 3: 4}
{'1':2, '3':4}

속성 이름으로 숫자를 사용하려고하면 자바스크립트 런타임은 문자열로 변환한다

 

x = [] // 배열 선언
x = [1,2,3] // 배열 할당
x[0] // 호출 : 1
x['1'] // 호출 : 2
Object.keys(x) // ['0','1','2']

문자열을 사용해도 배열에 접근할 수 있고, 배열의 Key 를 나열하면 키가 문자열로 출력된다

 

 

타입스크립트는 위와 같은 혼란을 바로잡기 위해 숫자키를 허용한다. 런타임에는 문자열 키로 인식하지만, 타입체크시점에 오류를 잡을 수 있다.

interface Array<T>{
    [n: number] : T;
}

 

 

const xs = [1,2,3];
const keys = Object.keys(xs); //타입 : string[]

for (const key in xs){ // for-in 
    key; // '1' 타입 : string
    const x = xs[key]; //xs[1] : 2
}

Object.keys 는 여전히 문자열로 반환한다. 마지막 줄에서는 string 이 number 에 할당되고있다 (실용적인 허용으로, 좋은 배열을 순회하는데 좋은 방법은 아닌다)

 

 

//인덱스 타입이 중요하지 않다면 for-of
for (const x of xs){ // xs 요소들을 반복하면서 x 에 할당
    x;
}

//인덱스의 타입이 중요하다면 forEach
xs.forEach((x,i)=>{ 
    i; // 현재 요소의 인덱스 : number
    x; // x로 할당됨 : number
});

//중간에 멈춰야한다면
for(let i=0; i<xs.length; i++){
    const x = xs[i];
    if(x<0) break;
}

 

타입이 불확실할 때 for in 루프는 for-of, C 스타일 for 에 비해 느리다.

 

인덱스 시그니처가 number 로 표현되어있다면 입력한 값이 number 여야 하지만 실제 런타임에서는 string 이다

const row: {[columnNum: number]: number}

 

 

number 를 인덱스 타입으로 사용하면 숫자 속성이 의미를 지닌다는 오해를 불러일으킬 수 있으니

숫자를 사용해 인덱스할 항목을 지정할 땐 Array 또는 튜플 타입을 사용한다

const row: number[] = [1, 2, 3];

type Row = [number, number, number];
const row: Row = [1, 2, 3];

 

배열과 비슷한 형태의 튜플을 사용하고 싶다면 ArrayLike 타입을 사용한다

function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
  if (i < xs.length) {
    return xs[i];
  }
  throw new Error(`배열의 끝을 지나서 ${i}를 접근하려고 했습니다`);
}

ArrayLike 는 배열과 비슷한 객체를 나타내는 타입으로, length 와 인덱스를 가지고있다

interface ArrayLike<T> {
    readonly length: number; //객체의 요소 갯수
    readonly [n: number]: T; // 인덱스 시그니처를 통해 해당 위치 요소값을 가져옴
}

 

요약

  • 배열은 객체이므로 키는 수자가 아니라 문자열이다. 인덱스 시그니처로 사용된 number 타입은 버그를 잡기 위한 순수 타입스크립트 코드이다.
  • 인덱스 시그니처에 number 를 사용하기 보다 Array 나 튜플, 또는 ArrayLike 타입을 사용하는 것이 좋다