[Effective Typescript] 타입스크립트의 타입 시스템(3)

이예슬·2022년 11월 15일
0

Effective TypeScript

목록 보기
6/15

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

자바스크립트 객체는 문자열 키를 타입의 값에 관계없이 매핑한다.

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

type Rocket = {[property: string] : string} 
const roket : Rocket = {
	name: 'roket1',
	variant: 'v1.0',
	thrust: '4,940 kN'
} 

위와 같이 타입 체크가 수행되면 네 가지 단점이 있다.

  • 잘못된 키를 포함한 모든 키를 허용한다.
  • 특정 키가 필요하지 않다. ({} 도 유효한 Rocket)
  • 키마다 다른 타입을 가질 수 없다.
  • 언어서비스가 완전히 동작하지 않는다.

이러한 단점들로 인해 인덱스 시그니처는 보통 동적인 데이터에 대한 타입을 선언할 때만 주로 사용된다.

즉 런타임 때까지 객체의 속성을 알 수 없는 경우에 인덱스 시그니처를 사용한다.

예를 들어 CSV 파일처럼 헤더 행에 열 이름이 있고 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶을 경우 인덱스 시그니처를 유용하게 사용할 수 있다.

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

선언해 둔 열들이 런타임에 실제로 일치한다는 보장은 없으므로 이 부분이 걱정된다면 undefined를 추가할 수 있다.

물론 모든 행들에 대한 데이터 타입을 알고 있을 경우에는 interface나 type을 사용하는 것이 더 안전하다.

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

자바스크립트에서 객체란 키/값 쌍의 모음이다. 이 때 키는 ES2015 이후로는 심벌도 가능하지만 주로 문자열이다. 숫자는 키로 사용할 수 없다. 하지만 타입스크립트는 숫자 키를 허용하며 문자열 키와 다른 것으로 인식한다.

const xs = [1, 2, 3]

const keys = Object.keys(xs) // 타입이 string[]
for(const key in xs) {
	key;// 타입이 string
	const x = sx[key] // 타입이 number 
} 

string은 number에 할당될 수 없으므로 위 코드의 마지막 줄에서 type error가 발생할 것이라고 생각할 수 있지만 이는 배열을 순회하는 코드 스타일에 대한 실용적인 허용이다.

하지만 위 코드는 배열을 순회하기에 좋은 방법은 아니다. 배열을 순회할 때 인덱스에 신경 쓰지 않아도 된다면 for in 보다는 for of 를 사용하는 것이 더 좋다.

인덱스의 타입이 중요한 경우 forEach나 for(;;) 루프를 사용하는 것이 좋다. 타입이 불활실할 경우 for in 은 이 세 가지 방법보다 몇 배나 느리기 때문이다.

또한 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, 또는 ArrayLike 타입을 사용하는 것이 좋다.

아이템 17. 변경 관련된 오류 방지를 위해 readonly 사용하기

타입스크립트에는 readonly 라는 키워드가 존재한다. 이름에서 알 수 있듯 함수가 매개변수로 받는 값을 변경없이 그대로 사용해야 할 경우 유용하게 사용할 수 있다.

function arraySum(arr: number[]): number {
  let sum =0, num;
  while((num = arr.pop()) !== undefined) {
    sum += num;
  }
  return sum;
}

위 코드는 배열이 주어졌을 때 배열 안의 원소를 모두 더하는 코드이다. 그런데 계산이 끝나면 원래 배열이 전부 비게 되는 의도하지 않은 결과가 발생하게 된다. 자바스크립트 배열은 pop(), push() 등의 배열을 속성을 변경하는 메서드를 통해 배열의 내용을 변경할 수 있기 때문이다.

이처럼 의도하지 않은 오류를 발생시키지 않기 위해 readonly 키워드를 사용할 수 있다.

function arraySum(arr: readonly number[]): number {
  let sum =0, num;
  while((num = arr.pop()) !== undefined) { // ❗ readonly number[] 형식에 pop 속성이 없습니다. 
    sum += num;
  }
  return sum;
}

위의 코드는 실행시 readonly 배열에는 pop 속성이 없다는 에러를 출력한다. 이 때 readonly number[] 는 타입으로 number[]와 구분되는 특징이 있다.

  • 배열의 요소를 읽을 수 있지만 쓸 수는 없다.
  • length를 읽을 수 있지만, 바꿀 수는 없다.
  • 배열을 변경하는 pop을 비롯한 다른 메서드를 호출할 수 없다.

readonly 키워드는 다음과 같이 작동한다.

  1. 타입스크립트가 함수 내에서 매개변수 값이 변경되는지 아닌 지 검사한다.
  2. 함수를 호출하는 쪽에서는 함수가 매개변수를 변경하지 않겠다는 약속을 받는다.

만약 함수가 매개변수를 변경하지 않는다면, readonly로 선언해야 하며 어떤 함수를 readonly 로 만들면 그 함수를 호출하는 다른 함수도 모두 readonly로 만들어야 한다.

const와 readonly의 차이

constreadonly 는 초기 할당된 값을 변경할 수 없다는 공통점이 있지만 몇 가지 다른 점이 존재한다.

먼저 const 는 변수 참조를 위한 것이며 readonly 는 속성을 위한 것이다.

const constObject = {
	property: 'foo'
} 
constObject.property = 'bar' // ⭕ 변경 가능 

type ReadonlyObject = {
	readonly property : string;
}

const readonlyObject : ReadonlyObject = {
	property: 'foo'
} 
readonlyObject.property = 'bar' // ❌ cannot assign to 'property' because it is a read-only property

하지만 readonly는 얕게 동작하므로 객체의 readonly 배열이 있다고 해서 그 객체 자체가 readonly 인 것은 아니다. 그러므로 깊은 readonly 타입을 사용하고 싶다면 제너릭을 직접 만들거나 ts-essentials에 있는 DeepReadonly 제너릭을 사용하면 된다.

profile
꾸준히 열심히!

0개의 댓글