[이펙티브 타입스크립트] 아이템 33 - string 타입보다 더 구체적인 타입 사용하기

Minha Ahn·2023년 8월 11일
1
post-thumbnail

💣string의 문제점

  1. string 타입의 범위가 너무 넓다. 아래의 예시들이 모두 string 타입
    • “x”
    • “Hello, World!”
    • “Call me Ishmael…” (모비 딕 전체 내용)
  2. string 타입의 남발은 오류를 발견하지 못할 수 있다.
    • 문자열을 남발하여 선언되었다. === “stringly typed”

⇒ string은 any와 비슷한 문제를 가지고 있으므로 범위를 제한해서 사용하는 것을 추천한다.

/** string 타입이 남발된 Album 타입 정의 */
interface Album {
	artist: string;
	title: string;
	releaseDate: string;     // YYYY-MM-DD 형식으로 들어간다.
	recordingType: string;   // "live" 혹은 "studio" 값만 들어간다.
};

/** 문제상황 1 */
// releaseDate와 recordingType 형식이 달라도 모두 string 타입이기 때문에 타입 체커 통과
const kindOfBlue: Album = {
	artist: 'Mile Davis',
	title: 'Kind of Blue',
	releaseDate: 'August 17th, 1959',  // YYYY-MM-DD 형식이 아니다!
	recordingType: 'Studio',           // "live" 혹은 "studio" 이외의 값이 들어갔다!
};

/** 문제상황 2 */
// 매개변수 순서가 잘못되어도 둘다 문자열이기 때문에 타입 체커 통과
function recordRelease(title: string, date: string) { /** ... */ }
recordRelease(kindOfBlue.releaseDate, kindOfBlue.title); // 매개변수 순서가 잘못되었다!

🕹️타입의 범위를 좁혀서 사용하기

  • 특정한 형식이 있다면 적극 사용하기 (ex. Date 객체)
  • 특정 문자열만 들어온다면 문자열 리터럴 타입의 유니온 사용하기
type RecordingType = 'studio' | 'live';

interface Album {
	artist: string;
	title: string;
	releaseDate: Date;            // Date 객체로 날짜 형식으로 제한
	recordingType: RecordingType; // 두 가지의 값만 들어올 수 있도록 제한
}

✨타입의 범위를 좁혀서 사용했을 때의 장점

  1. 타입을 명시적으로 정의함으로써 다른 곳으로 값이 전달되어도 타입 정보 유지
    • 이미 타입을 좁혀서 정의해뒀기 때문에 타입을 느슨하게 정의해도 타입 정보는 유지된다!
function getAlbumsOfType (recordingType: string): Album[] { /** ... */ }
  1. 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석 추가
/** 이 녹음은 어떤 환경에서 이루어졌는지? */
type RecordingType = "studio" | "live";

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: RecordingType;
}

function getAlbumsOfType(recordingType: RecordingType): void {
  console.log(recordingType);
}

  1. keyof 연산자로 더 세밀하게 객체의 속성 체크 가능

🔍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 타입은 지양해야하니 제너릭 타입 도입으로 개선해보자.
function pluck<T>(records: T[], key: string): any[] {
	return records.map(r => r[key]);
}

// keyof 연산자로 key의 범위를 string보다 더 제한해보자.
// keyof Album -> 타입이 'artist' | 'title' | 'releaseData' | 'recordingType'
function pluck<T>(records: T[], key: keyof T): T[keyof T][] {
	return records.map(r => r[key]);
}
  • 여전히 pluck 함수의 결과의 타입 범위가 넓다. 더 줄일 수 있을까?

text (string | Date)[]가 나오는 이유는 Album 타입의 ‘artist’, ‘title’, ‘releaseDate’, ‘recordingType’이 string 혹은 Date 타입이기 때문이에요.

function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] {
	return records.map(r => r[key]);
}
// keyof T : T에 속하는 유니온 타입
// K extends : 에서 타입 K를 받아
// T[K][] : 매칭되는 속성의 값 배열만 리턴한다.
  • 매개변수 타입이 정밀해져서 자동 완성 기능도 제공받을 수 있다.

💡결론

  • string 타입보다는 더 구체적인 타입을 사용하자.
  • string 타입보다는 문자열 리터럴 타입의 유니온을 사용하면 타입 체크를 더 엄격히 할 수 있고 생산성을 향상시킬 수 있다.
  • 객체의 속성 이름을 함수 매개변수로 받을 때는 string보다 keyof T를 사용하는 것이 좋다.
profile
프론트엔드를 공부하고 있는 학생입니다🐌

1개의 댓글

comment-user-thumbnail
2023년 8월 11일

훌륭한 글 감사드립니다.

답글 달기