About
string
타입의 범위는 너무 넓다.
'x'
, 'y'
같은 한 글자부터, 약 120만 글자에 달하는 소설 '모비 딕'(Moby Dick)의 전체 내용도 모두 string 타입이다.
예를 들면,
Copy 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
의 범위가 넓어서 함수의 매개변수 순서가 잘못된 것이 오류로 드러나지 않는다.
이렇게 string
이 남발된 코드를 "문자열을 남발하여 선언되었다( stringly typed
)" 라고 표현하기도 한다.
이렇게 바꿔보자.
Copy 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' 형식에 할당할 수 없습니다.
}
이러한 방식에는 세 가지 장점이 있다.
첫 번째, 타입을 명시적으로 정의함으로써 다른 곳으로 값이 전달되어도 타입이 유지 된다. 예를 들어, 특정 레코딩 타입의 앨범을 찾는 함수를 작성한다면 다음과 같이 작성할 수 있다.
Copy function getAlbumsOfType (recordingType : string ) : Album [] { /* ... */ }
두 번째, 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙일 수 있다. (Item 48)
Copy /** 이 녹음은 어떤 환경에서 이루어졌는지? */
type RecordingType = 'studio' | 'live'
이제 getAlbumsOfType
이 받는 매개변수를 RecordingType
으로 바꾸면, IDE/편집기 차원에서 함수를 사용하는 곳에서 주석을 볼 수 있게 된다.
세 번째, keyof
연산자로 더욱 세밀하게 객체 속성 체크가 가능 해진다.
언더스코어(Underscore) 라이브러리의 pluck 함수를 살펴보자.
Copy function pluck (records , key) {
return records .map (r => r[key])
}
위는 아래와 같이 타입을 부여할 수 있다.
Copy function pluck (records : any [] , key : string ) : any [] {
return records .map (r => r[key])
}
타입 체크가 되긴 하지만 any 타입이 있어 정밀하지 못하다. 특히 Item 38에 나오겠지만 반환 값에 any를 쓰는 건 좋지 않은 설계이다.
타입 시그니처를 개선하기 위해 제너릭 타입을 도입하자.
Copy 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
타입으로 얻는 예시이다.
Copy type K = keyof Album // 'artist' | 'title' | 'releaseDate' | 'recordingType'이 된다
이제 string
을 keyof T
로 바꾸면 된다.
Copy 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 T
는 string
에 비하면 범위가 좁긴 하지만 여전히 넓다.
범위를 좁히기 위해 keyof T
의 부분 집합으로 제너릭 매개변수를 추가하자. 더 좁혀보자.
Copy 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
를 사용하는 것이 좋다.