string 타입의 범위는 매우 넓다. string 타입으로 변수를 선언할 때, 그보다 더 좁은 타입이 있는지 생각해보자.
📍 예시
🔗 음악 컬렉션을 위한 앨범 타입 정의
interface Album {
artist: string;
title: string;
releaseDate: string; // YYYY-MM-DD
recordingType: string; // E.g., "live" or "studio"
}
❓ string 타입이 남발되었다. 주석에 타입 정보를 적어둔 걸 보면 현재 인터페이스가 잘못되었다는 것을 알 수 있다.
→ Album 타입에 엉뚱한 값을 설정할 수 있다.
const kindOfBlue: Album = {
artist: 'Miles Davis',
title: 'Kind of Blue',
releaseDate: 'August 17th, 1959', // Oops! // 날짜 형식 다름
recordingType: 'Studio', // Oops! // live or studio
}; // OK
잘못된 값이 설정되었지만 두 값 모두 문자열이므로, Album 타입에 할당 가능하며 타입 체커를 통과한다.
❓ string 타입의 범위가 넓어 Album 객체를 사용하더라도 매개변수 순서가 잘못된 것이 오류로 드러나지 않는다.
function recordRelease(title: string, date: string) { /* ... */ }
recordRelease(kindOfBlue.releaseDate, kindOfBlue.title); // OK, should be error
recordRelease의 매개변수 순서가 바뀌었지만, 둘 다 string이므로 타입 체커가 정상으로 인식한다.
⭐️ 타입의 범위를 좁히자.
✓ artist나 title같은 필드는 string이 적절하다.
✓ releaseDate 필드는 Date 객체를 사용해서 날짜 형식으로만 제한하자.
✓ recordingType 필드는 "live" or "studio", 두 개의 값으로 유니온 타입을 정의하자. (enum은 추천X)
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'
// ~~~~~~~~~~~~ Type '"Studio"' is not assignable to type 'RecordingType'
};
⭐️ 이러한 방식의 세 가지 장점
✓ 타입을 명시적으로 정의하여 다른 곳으로 값이 전달되더라도 타입 정보가 유지된다.
// 특정 레코드 타입의 앨범을 찾는 함수
function getAlbumsOfType(recordingType: string): Album[] {
// ...
}
recordingType의 값이 string 타입인 것 외에 정보가 없다.
✓ 타입을 명시적으로 정의하고, 해당 타입의 의미를 설명하는 주석을 붙여 넣을 수 있다.
/** What type of environment was this recording made in? */
type RecordingType = 'live' | 'studio';
매개변수를 string 대신 Recording 타입으로 바꾸면, 함수를 사용하는 곳에서 RecordingType의 설명을 볼 수 있다.
✓ keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능하다.
함수의 매개변수에 string을 잘못 사용하는 일은 흔하다. 어떤 배열에서 한 필드의 값만 추출하는 함수를 작성해보자.
// 언더스코어 라이브러리 pluck 함수
function pluck(records, key) {
return records.map(r => r[key]);
}
// pluck 함수의 시그니처
function pluck(records: any[], key: string): any[] {
return records.map(r => r[key]);
}
❓ 타입 체크가 되지만 any 타입으로 인해 정밀함이 떨어진다. 특히, 반환값에 any를 사용하는 것은 좋지 않다.
⭐️ 타입 시그니처 개선을 위해 제너릭 타입 도입
function pluck<T>(records: T[], key: string): any[] {
return records.map(r => r[key]);
// ~~~~~~ Element implicitly has an 'any' type
// because type '{}' has no index signature
}
❓ key의 값이 string이므로 범위가 너무 넓다는 오류 발생
key는 단 네 개의 값("artist", "title", releaseData", "recordingType")만이 유효하다. 다음 예시는 keyof Album 타입으로 얻게 되는 결과이다.
type K = keyof Album;
// ^? type K = keyof Album
// (equivalent to "artist" | "title" | "releaseDate" | "recordingType")