반응형
아이템 31 - 타입 주변에 null 값 배치하기
- 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하면 안 됨
- API 작성 시에는 반환 타입을 큰 객체로 만들고, 반환 타입 전체가 null이거나 null이 아니게 만들어야 함
- 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋음
strictNullChecks
는 반드시 필요함
function extent(nums: number[]) {
let min, max;
for (const num of nums) {
if (!min) {
min = num;
max = num;
} else {
min = Math.min(min, num);
max = Math.max(max, num);
}
}
return [min, max];
}
// 최소값이나 최대값이 0인 경우, 값이 덧씌워짐
// nums 배열이 비어있다면 [undefined, undefined] 반환 > 반환 타입이 (number | undefined)[]로 추론됨
// 설계의 문제: min에 대한 체크만 하게되어 max는 undefined가 나올 수 있음
function extent(nums: num[]) {
let result: [number, number] | null = null;
for (const num of nums) {
if (!result) {
result = [num, num];
} else {
result = [Math.min(num, result[0]), Math.max(num, result[1])]
}
}
return result
}
// 반환 타입: [number, number] | null
- null인 경우가 필요한 속성은 프로미스로 바꾸면 안됨
- 모든 메서드를 비동기로 바꿔야 하기 때문
아이템 32 - 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
// 1번
interface Layer {
layout: FillLayout | LineLayout | PointLayout;
paint: FillPaint | LinePaint | PointPaint;
}
// 2번
interface FillLayer {
layout: FillLayout;
paint: FillPaint;
}
interface LineLayer {
layout: LineLayout;
paint: LinePaint;
}
interface PointLayer {
layout: PointLayout;
paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;
- 의미상 1번보다 2번의 정의가 맞을때, 이러한 패턴을 태그된 유니온(또는 구분된 유니온)이라고 말함
interface FillLayer {
type: 'fill';
...
}
interface LineLayer {
type: 'line';
...
}
interface PointLayer {
type: 'paint';
...
}
type Layer = FillLayer | LineLayer | PointLayer;
- Layer를 나타내는 type 속성을 추가하면 ‘태그’로서 작동하며 어떤 타입의 Layer가 쓰이는지 판단됨
- 또한 type을 판단하는 if문을 통해 범위를 좁히는데도 사용
- 동시에 값이 있거나 없을 경우에도 태그된 유니온 패턴이 잘 맞음
interface Person {
name: string;
// place와 date가 둘 다 동시에 있거나 동시에 없을 때, 하나의 객체로 모아서 설계
birth?: {
place: string;
date: Date;
}
}
- 타입 구조에 손대지 못할 때도 인터페이스 유니온을 사용해서 속성간의 관계를 정의해줄 수 있음
interface Name {
name: string;
}
interface PersonWithBirth extends Name {
placeOfBirth: string;
dateOfBirth: Date;
}
type Person = Name | PersonWithBirth;
아이템 33 - string 타입보다 더 구체적인 타입 사용하기
- 문자열을 남발하여 선언된 코드를 피해야 함 /
string
타입보단 구체화된 타입을 사용하는 것이 좋음 - 문자열 리터럴 타입의 유니온을 사용해 타입 체크를 엄격히 하도록 해야함
- 객체의 속성 이름을 매개변수로 받을 때는
keyof T
를 사용하는 것이 좋음
BAD
interface Album {
artist: string;
title: string;
releaseDate: string; // YYYY-MM-DD
recordingType: string; // E.g., "live" or "studio"
}
GOOD
type RecordingType = 'studio' | 'live';
interface Album {
artist: string;
title: string;
releaseDate: Date;
recordingType: RecordingType;
}
이러한 방식의 장점
- 명시적으로 정의함으로써 다른곳으로 값이 전달되어도 타입 정보가 유지
type RecordingType = 'studio' | 'live';
function getAlbumsOfType(recordingType: string): Album[] {
// ...
}
/* getAlbumsOfType에서는 아무런 타입정보를 확인할 수 없지만 RecordingType을 통해 정의를 확인할 수 있다. */
- 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙여 넣을 수 있음
/** 이 녹음이 어떤 환경에서 이루어 졌는지 확인하는 타입 */
type RecordingType = 'live' | 'studio';
keyof
연산자로 더욱 세밀하게 객체의 속성 체크가 가능
function pluck(record: any[], key: string): any[] {
return record.map(r => r[key]);
}
/* any 타입이 있어서 정밀하지 못함. 특히나 return 타입이 any가 사용되어 좋지 않은 설계입니다. */
function pluck<T>(record: T[], key: string): any[] {
return record.map(r => r[key]);
// ~~~~~~ '{}' 형식에 인덱스 시그니처가 없으므로
// 요소에 암시적으로 'any' 형식이 있습니다.
}
/* 제네릭타입을 도입하면 key의 범위가 너무 넓어 오류가 발생합니다. (4개의 값만 유효) */
// "artist" | "title" | "releaseDate" | "recordingType"
interface Album {
artist: string;
title: string;
releaseDate: Date;
recordingType: RecordingType;
}
function pluck<T>(record: T[], key: keyof T) {
return record.map(r => r[key]);
}
declare let albums: Album[];
const releaseDates = pluck(albums, 'releaseDate'); // 타입이 (string | Date)[]
/* 타입 체커를 통과하며 타입 추론도 진행됩니다. 하지만 여전히 범위가 넓습니다.*/
function pluck<T, K extends keyof T>(record: T[], key: K): T[K][] {
return record.map(r => r[key]);
}
/* 제네릭을 2개 사용하여 부분집합을 표현하였습니다. 더욱 타입 시그니처가 완벽해졌습니다. */
결론
- string은 any와 같이 넓은 범위를 허용하여 타입간의 관계를 감추므로, string의 부분 집합을 정의하여 보다 정확한 타입을 사용해야 함
아이템 34 - 부정확한 타입보다는 미완성 타입을 사용하기
- 예를 들어 경도와 위도를 나타내는 number 배열을 선언하는 경우에
number[]
보다는 튜플 타입[number, number]
로 선언하는것이 나음 - 타입을 구체적으로 정의한다고 해서 정확도가 무조건 올라가지는 않음
- any를 지양하되 부정확한 모델링은 피해야 함
아이템 35 - 데이터가 아닌, API와 명세를 보고 타입 만들기
- 파일 형식, API, 명세 등 외부에서 비롯된 타입인 경우, 직접 작성하지 않고 자동으로 생성할 수 있음
- 이 때 핵심은, 데이터가 아닌 명세를 참고해 타입을 생성해야 함
- 예외 상황은 조건을 분기해 헬퍼 함수를 호출해 모든 타입을 지원하도록 할 수 있음
아이템 36 - 해당 분야의 용어로 타입 이름 짓기
- 엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드와 타입의 추상화 수준을 높여줌
동물들의 데이터베이스를 구축한다 가정
interface Animal {
name: string;
endangered: boolean;
habitat: string;
}
const leopard: Animal = {
name: 'Snow Leopard',
endangered: false,
habitat: 'tundra'
}
- 해당 코드의 문제점
- name은 매우 일반적인 용어이므로 범위가 너무 포괄적임
- endangered 멸종 위기를 표현하는 변수를 boolean으로 설정하면, 이미 멸종된 동물에 값을 부여하는 것이 애매함
- habitat은 범위가 매우 넓은 string 타입일 뿐만 아니라, 뜻 자체도 애매함
- 객체의 변수명과 name 속성의 의도가 불분명함
개선
interface Animal {
commonName: string;
genus: string;
species: string;
status: ConservationStatus;
climates: KoppenClimate[];
}
- name은 commonName, genus, species 등 더 구체적인 용어로 대체
- endangered는 동물 보호 등급에 대한 IUCN 표준 분류 체계 ConservationStatus로 변경
- habitat은 기후를 뜻하는 climates로 변경되었으며, 쾨펜 기후 분류를 사용
주의사항
- 동일한 의미를 나타낼 때는 같은 용어를 사용해야 함 / 의미적으로 구분이 되어야 하는 경우에만 다른 용어를 사용
- 정식 명칭이 있는 경우 해당 명칭을 사용해야 함
- 의미 없는 이름을 붙이면 안됨
- 이름을 지을 때 포함된 내용이나 계산 방식이 아닌, 데이터 자체가 무엇인지 고려해야 함
아이템 37 - 공식 명칭에는 상표를 붙이기
interface Vector2D {
_brand: '2d'; // 상표기법
x: number;
y: number;
}
function vec2D(x: number, y: number): Vector2D {
return {x, y, _brand: '2d'}
}
function calculateNorm(p: Vector2D) {
return '~~~'
}
상표를 통해 함수가
Vector2D
타입만 받는 것을 보장타입 시스템에서 동작하지만, 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있음
타입스크립트는 구조적 타이핑을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있음
따라서 값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 함
아이템 38 - any 타입은 가능한 한 좁은 범위에서만 사용하기
function processBar(b: Bar) {}
function f() {
const x = expressionReturningFoo();
processBar(x); // 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}
문맥상 x라는 변수가 동시에 Foo 타입과 Bar 타입에 할당 가능하다면, 오류를 제거하는 방법은 두 가지
- 변수에
any
타입 지정const x: any = ~~
- 인자에 전달 시
any
단언f(x as any)
2번을 사용해야 하는 이유: 함수의 매개변수에서만 사용된 단언이므로 다른 코드에 영향을 미치지 않음
1번을 사용했을 때 문제가 되는 경우
function f1() {
const x: any = expressionReturningFoo();
processBar(x);
return x;
}
function g() {
const foo = f1(); // 타입이 any
foo.fooMethod(); // 체크되지 않음
}
비슷한 관점에서 함수의 반환 타입을 추론할 수 있는 경우에도 반환 타입을 명시하는 것이 좋음.
즉, g
함수의 foo
는 any
타입이기 때문에 g
함수를 쓰는 곳이라면 연쇄적으로 영향을 미칠 수 있음
추가적으로, any
를 사용해야 한다면 최소한의 범위에서 사용해야하며 차라리 @ts-ignore
가 나음
아이템 39 - any를 구체적으로 변형해서 사용하기
any보다 더 구체적인 타입을 찾아 타입 안정성을 높여야 함
// 1. any 타입의 값을 그대로 정규식이나 함수에 넣는 것은 권장되지 않음
function getLengthBad(array: any) {} // x
function getLengthBad(array: any[]) {
return array.length
} // o
array.length
타입이 체크됨- 리턴 타입이 number로 추론됨
- 매개변수가 배열인지 체크됨
any의 구체화
배열의 배열 형태 - any[][]
내용을 모르는 객체 - {[key: string]: any}
{[key: string]: any}
와 object
의 차이
object 타입은 객체의 키를 열거할 수는 있지만, 속성에 접근할 수 없음
any를 구체화 하는 방법
type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type Fn2 = (...arg: any[]) => any; // 모든 개수의 매개변수
즉, any보다 더 정확하게 모델링할 수 있도록 구체적인 형태를 사용해야 함
아이템 40 - 함수 안으로 타입 단언문 감추기
- 타입 단언문이 드러나 있는 것보다, 제대로 타입이 정의된 함수 안으로 단언문을 감추는 것이 더 좋은 설계
declare function shallowEqual(a: any, b: any): boolean;
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== b[k]) {
return false
}
}
}
// k in b 체크로 b 객체어 k 속성이 있다는 것을 확인했지만, 오류가 발생
// 실제 오류가 아니기 때문에 이럴 때 any로 단언을 해야 함
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== (b as any)[k]) {
return false
}
}
}
출처: 이펙티브 타입스크립트