221027 TIL | 이펙티브 타입스크립트_#7 동적 데이터에 인덱스 시그니처 사용하기 등

아이템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}; 부분이 인덱스 시그니처 세 가지 의미를 가진다.
    1. 키의 이름 : 키의 위치를 표시하는 용도.
    2. 키의 타입 : string, number, symbole을 사용할 수 있으며 보통은 string 사용
    3. 값의 타입 : 어떤 것이든 될 수 있음

인덱스 시그니처의 단점

  1. 잘못된 키를 포함해 모든 키를 허용한다
    • ex ) name 대신 Name (O)
  2. 특정 키가 필요하지 않다.
    • ex ) {}도 Rocket 타입 (O)
  3. 키마다 다른 타입을 가질 수 없다.
    • ex ) thrust가 number여야 한다면 사용하지 못 한다.
  4. 타입스크립트의 언어 서비스를 사용할 수 없다.
    • 자동 완성 등

인덱스 시그니처의 사용처

  • 타입스크립트에서 제공하는 언어 서비스를 사용하기 위해서는 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.`)
}

출처

  • 이펙티브 타입스크립트