Table of contents
아이템28. 유효한 상태만 표현하는 타입을 지향하기
아래의 예시는 웹 애플리케이션의 전체 상태에 대한 타입 설계 입니다. 웹 페이지의 상태가 될 수 있는 값들을 정확하게 파악하고 모호함(예시: A상태이자 B상태인)이 없도록 명확하게 구분했습니다.
interface RequestPending {
state: 'pending';
}
interface RequestError {
state: 'error';
error: string;
}
interface RequestSuccess {
state: 'ok';
pageText: string;
}
// 상태값은 세 가지 중 반드시 하나를 가져야함
type RequestState = RequestPending | RequestError | RequestSuccess;
interface State {
currentPage: string;
requests: {[page: string]: RequestState}
}
유효한 상태와 무효한 상태 두 가지를 모두 표현하는 타입은 혼란이 있을 수 있습니다. 유효한 상태만을 표현하도록 타입을 설계해야 합니다.
아이템29. 사용할 때는 너그럽게, 생성할 때는 엄격하게
함수를 구현할 때, 전달 받는 매개변수의 타입은 범위가 넓어도 되지만 결과를 반환할대는 타입의 범위가 구체적이여야합니다. 조금 더 구체적으로는, 매개변수는 옵셔널 값을 받거나 여러 타입을 받을 수 있도록 설계하고, 반환하는 값은 최대한 옵셔널 값을 지향하고 명확한 타입이여야 한다는 의미입니다.
// 기존: 옵션 타입의 내부 필드 또한 부분적으로 옵션이며, viewportForBounds함수의 반환 타입 또한 CameraOptions이기 때문에 호출 후에 반환되는 값을 사용할 때, 타입 체크/옵셔널 타입에 대한 체크 등 사용이 불편하고 버그를 생산할 수도 있습니다.
// 옵션 타입
interface CameraOptions {
// LngLat 타입 또한 여러 옵션을 제공하는 별도의 타입 설계
center?: LngLatBounds;
zoom?: number;
bearing: number;
pitch: number;
}
declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;
// 개선: 파라미터의 타입을 보다 더 유연하게 받아들일수 있도록 옵션 타입에 옵셔널 필드를 선언하고, 명확하게 계산된 Camera타입을 반환합니다.
// 옵션 타입
interface CameraOptions {
// LngLat 타입 또한 여러 옵션을 제공하는 별도의 타입 설계
center?: LngLatLike;
zoom?: number;
bearing?: number;
pitch?: number;
}
// 타입
interface Camera {
center: LngLatBounds;
zoom: number;
bearing: number;
pitch: number;
}
declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
아이템 30. 문서에 타입 정보를 쓰지 않기
JSDOC
을 사용하여 주석을 작성할 때 타입 정보를 포함하는 것을 지양해야합니다. 문서와 주석의 정보가 일치하지 않을 수 있기 때문입니다. 함수의 반환타입은 시그니처 작성시에 충분히 표현될 수 있도록 해야 합니다. 특히 readonly
, private
등의 부가정보를 충분히 작성하여 코드로서 타입을 표현하는 것이 좋습니다.
아이템31. 타입 주변에 null 값 배치하기
Javascript
런타임 구동 시에 undefined
와 관련된 오류를 상당히 많이 접하게 됩니다. 대부분은 의도되지 않은 경우이며 버그인 경우가 많습니다. Typescript
에서 타입을 다룰 때 undefined
를 배제할 수 있도록 코드를 작성하는 것이 좋습니다. 컴파일러의 strictNullChecks
옵션을 켜는 것을 권장하며, 함수를 작성할 때 반환 타입은 null
이거나 null
이 아니거나 둘 중 하나인 상태로 하는 것이 좋습니다.
아이템32. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
유니온 타입의 속성을 여러 개 가지는 인터페이스는 속성간의 관계에 따라 유효하지 않은 상태를 표현하게 될 수 있으므로 지양해야합니다. 따라서 이러한 경우가 있다면 더 작은 상태를 표현하는 인터페이스를 설계하고 이 인터페이스들의 유니온으로 타입을 설계하는 방식이 좋습니다.
// 벡터를 그리는 프로그램의 도형을 구성하는 요소의 타입을 설계
// 기존: 레이어 타입 내에 레이아웃과 페인트의 타입을 설계함.
// 레이아웃과 페인트의 값이 유효하지 않게 설정될 수 있는 문제가 있음.
interface Layer {
layout: FillLayout | LineLayout | PointLayout;
paint: FillPaint | LinePaint | PointPaint;
}
// 개선: 각각의 레이어 타입을 설계하여 유효하지 않은 레이아웃, 페인트가 설정될 수 있는 문제를 방지함
interface FillLayer {
layout: FillLayout;
paint: FillPaint;
}
interface LineLayer {
layout: LineLayout;
paint: LinePaint;
}
interface PointLayer {
layout: PointLayout;
paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;
아이템33. string
타입보다 더 구체적인 타입 사용하기
속성이 string
타입이지만, 할당될 수 있는 값의 한계가 있다면, 이를 타입으로 선언하여 사용하는 것이 좋습니다.
// 기존
interface Album {
artist: string;
title: string;
releaseDate: string; // YYYY-MM-DD
recordingType: string; // "live" or "studio"
}
const someAlbum: Album = {
artist: 'Yoon Mirea',
title: 'Why',
releaseDate: 'August 17th, 1959', // 날짜 형식이 다름
recordingType: 'Studio' // S가 대문자로 오타이지만 오류가 아님
}
// 개선
/** 어디에서 녹음되었는지 */
type RecordingType = 'studio' | 'live'
interface Album {
artist: string;
title: string;
releaseDate: Date; // 값의 유효성을 체크
recordingType: RecordingType; // 값의 유효성을 체크
}
이와 같이 선언했을 때, 객체를 다른 곳으로 전달하더라도 타입 정보가 유지되어 유효하지 않은 값이 할당될 수 있는 여지가 줄어듭니다. 그리고 언어서비스인 자동완성 기능에 할당될 수 있는 값이 표시되고, 타입에 JSDOC
형식의 주석을 작성하여 타입에 대한 설명을 작성할 수도 있습니다. 또한 keyof
연산자를 통해서 속성을 다룰 때 타입을 정교하게 다룰 수 있게 됩니다.
아이템34. 부정확한 타입보다는 미완성 타입을 사용하기
정확하게 타입을 모델링할 수 없다면, 부정확하게 모델링 하지 말아야 합니다. any
와 같은 너무 추상적인 타입은 피해야하지만 너무 구체적인 타입은 오히려 개발 경험을 떨어뜨릴 수도 있습니다.
아이템35. 데이터가 아닌, API와 명세를 보고 타입 만들기
Typescript
로 외부의 서비스와 상호작용하거나, 외부 API를 이용할 때 예시 데이터를 기반으로 타입을 설계 하는 경우가 많이 있습니다. 데이터에 드러나지 않는 예외적인 경우가 발생할 수가 있기 때문에 데이터보다는 명세를 기반하여 코드를 작성하는 것이 좋습니다.
아이템36. 해당 분야의 용어로 타입 이름 짓기
가독성을 높이고, 추상화 수준을 올리기 위해서 해당 분야의 용어를 사용해야합니다. 자체적인 해석에 의존하여 용어를 재정의하게 되면 혼란이 생길 수 있습니다.
아이템37. 공식 명칭에는 상표(brand)를 붙이기
구조적 타이밍(덕 타이핑)의 특성으로 아래의 코드는 유효합니다.
interface Vector2D {
x: number;
y: number;
}
function calculateNorm(p: Vector2D) {
return Math.sqrt(p.x * p.x + p.y + p.y);
}
calculateNorm({x: 3, y: 4}); // 5
const vec3D = {x:3, y:4, z: 1};
calculateNorm(vec3D); // 정상, 5
오류가 없고 결과값이 제대로 계산되지만, 의도에는 맞지 않습니다. 이런 경우에는 객체 내부에 객체의 명칭(brand)를 붙여서 구분하도록 할 수 있습니다.
interface Vector2D {
_brand: '2D';
x: number;
y: number;
}
function vec2D(x: number, y: number): Vector2D {
return { x, y, _brand: '2D'};
}
calculateNorm(vec2D({x: 3, y: 4})); // 5
const vec3D = {x:3, y:4, z: 1};
calculateNorm(vec3D); // 오류
아래의 예시와 같이 정렬된 리스트를 파라미터로 받기 위해서 강제하는 기법으로도 활용할 수 있습니다.
type SortedList<T> = T[] & {_brand: 'sorted'};
function isSorted<T>(xs: T[]): xs is SortedList<T> {
for (let i=1; i<xs.length; i++)
if(xs[i] < xs[i-1]) return false;
return true;
}
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
// Todo: search x
}