Item 33 string 타입보다 더 구체적인 타입 사용하기

About

string 타입의 범위는 너무 넓다.

'x', 'y' 같은 한 글자부터, 약 120만 글자에 달하는 소설 '모비 딕'(Moby Dick)의 전체 내용도 모두 string 타입이다.

예를 들면,

interface Album {
  artist: string
  title: string
  releaseDate: string   // YYYY-MM-DD
  recordingType: string // e.g., 'live', 'studio'
}

const kindOfBlue: Album = {
  artist: 'Miles Davis',
  title: 'Kind of Blue',
  releaseDate: 'August 17th, 1959',  // 날짜 형식이 다릅니다.
  recordingType: 'Studio'            // 오타 (대문자 S)
}                                    // 그러나... 타입에 문제 없으므로 정상!

function recordRelease(title: string, date: string) { /* ... */ }
recordRelease(kindOfBlue.releaseDate, kindOfBlue.title) // 오류여야 하지만 정상
  • string 타입이 남발된 모습이다.

  • 게다가 주석에 타입 정보도 넣어 두었다. (Item 30 문서에 타입 정보를 쓰지 않기)

  • 근데 타입에 오류는 없어서 정상적으로 실행된다.

  • string의 범위가 넓어서 함수의 매개변수 순서가 잘못된 것이 오류로 드러나지 않는다.

  • 이렇게 string이 남발된 코드를 "문자열을 남발하여 선언되었다(stringly typed)"라고 표현하기도 한다.

이렇게 바꿔보자.

type RecordingType = 'studio' | 'live'
interface Album {
  artist: string
  title: string
  releaseDate: Date
  recordingType: RecordingType
}

const kindOfBlue: Album = {
  artist: 'Miles Davis',
  title: 'Kind of Blue',
  releaseDate: new Date('1959-08-17')
  recordingType: 'Studio'            
               // ~~~~~~ '"Studio"' 형식은 'RecordingType' 형식에 할당할 수 없습니다.
}

이러한 방식에는 세 가지 장점이 있다.

첫 번째, 타입을 명시적으로 정의함으로써 다른 곳으로 값이 전달되어도 타입이 유지된다. 예를 들어, 특정 레코딩 타입의 앨범을 찾는 함수를 작성한다면 다음과 같이 작성할 수 있다.

function getAlbumsOfType(recordingType: string): Album[] { /* ... */ }

두 번째, 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙일 수 있다. (Item 48)

/** 이 녹음은 어떤 환경에서 이루어졌는지? */
type RecordingType = 'studio' | 'live'

이제 getAlbumsOfType이 받는 매개변수를 RecordingType으로 바꾸면, IDE/편집기 차원에서 함수를 사용하는 곳에서 주석을 볼 수 있게 된다.

세 번째, keyof 연산자로 더욱 세밀하게 객체 속성 체크가 가능해진다.

언더스코어(Underscore) 라이브러리의 pluck 함수를 살펴보자.

function pluck(records, key) {
  return records.map(r => r[key])
}

위는 아래와 같이 타입을 부여할 수 있다.

function pluck(records: any[], key: string): any[] {
  return records.map(r => r[key])
}

타입 체크가 되긴 하지만 any 타입이 있어 정밀하지 못하다. 특히 Item 38에 나오겠지만 반환 값에 any를 쓰는 건 좋지 않은 설계이다.

타입 시그니처를 개선하기 위해 제너릭 타입을 도입하자.

function pluck<T>(records: T[], key: string): any[] {
  return records.map(r => r[key])
                       // ~~~~~~ '{}' 형식에 인덱스 시그니처가 없으므로
                       // 요소에 암시적으로 'any' 형식이 있습니다.
}

이제 타입스크립트는 key의 타입이 string이라 범위가 너무 넓다는 오류가 발생한다. Album의 배열을 매개변수로 전달하면 기존의 string 타입의 넓은 범위와 반대로, key는 단 네 개의 값('artist', 'title', 'releaseDate', 'recordingType')만이 유효하다.

keyof의 설명을 돕기 위해 준비된 다음 예시는 keyOf Album 타입으로 얻는 예시이다.

type K = keyof Album // 'artist' | 'title' | 'releaseDate' | 'recordingType'이 된다

이제 stringkeyof T로 바꾸면 된다.

function pluck<T>(records: T[], key: keyof T) { // T[keyof T][]로 추론된다.(객체 내 가능한 모든 타입)
  return records.map(r => r[key])               // 마우스를 올려보면 추론된 타입을 볼 수 있다.
}

const releaseDate = pluck(album, 'releaseDate') // 타입이 (string | Date)[]

이 코드는 타입 체커를 통과하며, 타입스크립트가 반환 타입을 추론할 수 있다.

하지만 keyof Tstring에 비하면 범위가 좁긴 하지만 여전히 넓다.

범위를 좁히기 위해 keyof T의 부분 집합으로 제너릭 매개변수를 추가하자. 더 좁혀보자.

function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] {
  return records.map(r => r[key])
}

pluck(albums, 'releaseDate') // 타입이 Date[]
pluck(albums, 'artist') // 타입이 string[]
pluck(albums, 'recordingType') // 타입이 RecordingType[]
pluck(albums, 'recordingDate')
            // ~~~~~~~~~~~~~ '"recordingDate"' 형식의 인수는
            //               ... 형식의 매개변수에 할당될 수 없습니다.

이제 타입 시그니처가 완벽해졌다. 언어 서비스는 Album의 키에 자동 완성 기능을 제공할 수 있게 되었다. (Item 42)

보다 정확한 타입을 사용하면 오류를 방지하고 코드의 가독성을 향상시킬 수 있다.

Summary

  • 'stringly typed' 코드를 피하자. 모든 문자열을 할당할 수 있는 string 타입보다는 더 구체적인 타입을 사용하자.

  • 변수의 범위를 보다 정확하게 표현하고 싶다면 string 타입보다는 문자열 리터럴 타입의 유니온을 사용하자.

    • 타입 체크를 더 엄격히 하고 생산성을 향상시킬 수 있다.

  • 객체의 속성 이름을 함수 매개변수로 받을 때는 string이 아닌 keyof T를 사용하는 것이 좋다.

Last updated