-
이펙티브 타입스크립트- 4장 타입설계 (2)TypeScript 2022. 4. 25. 23:37
아이템 31. 타입 주변에 null 값 배치하기
strictNullChecks 설정을 한 경우 null이나 undefined에 대한 처리가 많아 지지만 null에 대한 문제점을 찾을 수 있기에 꼭 필요하다.
function extend(nums: number[]) { let min, max; // type : undefined for(const num of nums) { if(!min) { min = num; max = num; } else { min = Math.min(min, num); max = Math.max(max, num); // error : max is undefined } return [min, max]; }
위 코드는 타입 체커를 통과하고(strictNullChecks없이), 반환 타입은 number[]로 추론되지만 버그와 함께 설계적 결함이 있다.
- min이 없을 경우 max에도 number로 추가해주었지만 max는 undefined로 추론된다.
- num이 0인 경우 if문에서 0은 false로 반환되므로 값이 덮어 씌여지는 현상이 발생한다.
- undefined를 포함하는 객체는 다루기 어렵고 권장하지 않는다. 그래서 strictNullChecks를 설정해야 하고 undefined를 없애기 위해 null을 사용하는 것을 권장한다.
strictNullChecks을 켰을 때 extent의 반환 타입이 (number | undefined)][]로 추론되어서 설계적 결함이 분명해졌다.
function extend(nums: number[]) { let result: [number, number] | null = null; for(const num of nums) { if(!result) { result = [num, num]; } else { result = [Math.min(result[0], num), Math.max(result[1], num)]; } } return result; }
이제 반환 타입이 [number, number] | null이 되어서 사용하기 수월해졌다(null이 아님 단언(!)을 사용하면 min과 max를 얻을 수 있다)
또한 null 아님 단언 대신 단순 if문으로 체크할 수 있다.
const range = extend([0, 1, 2]); if (range) { const [min, max] = range; const span = max - min; //pass }
요약
- 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하면 안된다.
- API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null이거나 null이 아니게 만들어야 한다.
- 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.
- strictNullChecks를 설정하면 코드에 많은 오류가 표시되겠지만, null 값과 관련된 문제점을 찾아낼 수 있기 때문에 반드시 필요하다.
아이템 32. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
유니온 타입의 속성을 가지는 인터페이스를 작성 중이라면 인터페이스의 유니온 타입을 사용하는 게 더 알맞지 않을 지 검토 해야한다.
아래의 예시를 보자
interface Layer { layout: FillLayout | LineLayout | PointLayout; paint: FillPaint | LinePaint | PointPaint; }
위 타입의 문제점은 LineLayount 이면서 FillPaint가 가능하다는 점이다.
이런 조합을 허용한다면 라이브러리에서는 오류가 발생하기 십상이고 인터페이스를 다루기도 어려워진다.
interface FillLayer { layout: FillLayout; paint: FillPaint; } interface LineLayer { layout: LineLayout; paint: LinePaint; } interface PointLayer { layout: PointLayout; paint: PointPaint; } type Layer = FillLayer | LineLayer | PointLayer;
위와 같이 바꾸면 잘못된 조합으로 섞이는 것을 방지할 수 있다.
이겨서 어떤 Layer가 왔는 지 구분하기 위해서 아이템 28에 따라 type을 지정하고 이를 구분할 수 있게 한다.
interface FillLayer { type: 'fill'; layout: FillLayout; paint: FillPaint; } interface LineLayer { type: 'line'; layout: LineLayout; paint: LinePaint; } interface PointLayer { type: 'point'; layout: PointLayout; paint: PointPaint; } type Layer = FillLayer | LineLayer | PointLayer;
긴밀한 속성이라면 묶어두자
interface Person { name: string; //다음은 둘 다 동시에 있거나 동시에 없습니다. placeOfBirth?: string; dateOfBirth?: Date; }
위의 placeOfBirth와 dateOfBirth 필드는 실제로 관련되어 있지만 타입정보에는 어떠한 관계도 표현되지 않았다.
이때 두 개의 속성을 하나의 객체로 모으는 것이 더 나은 설계이다.
interface Person { name: string; birth?: { place: string, date: Date, }; } const alanT: Person = { name: 'Alan Turing', birth: { //error not in date place: 'London', }, };
이제 brith타입중 하나만 있다면 모두 필요하다고 error가 표시된다. 또한 이렇게 타입을 지정한 경우 타입 하나만 체크하면 내부 속성을 모두 쓸수 있다는 장점이 있다.
function eulogize(p: Person) { console.log(p.name); const { birth } = p; if (birth) { console.log(`born ${birth.date} in ${birth.place} `); } }
요약
- 유니온 타입의 속성을 여려 개 가지는 인터페이스에서는 속성 간의 관계가 분명하지 않기 때문에 실수가 자주 발생하므로 유의
- 유니온의 인터페이스보다 인터페이스의 유니온이 더 정확하고 타입스크립트가 이해하기도 좋다.
- 타입스크립트 제어 흐름을 분석할 수 있도록 타입에 태그를 넣는 것을 고려해야 한다.
아이템 33. string 타입보다 더 구체적인 타입 사용하기
string 타입의 범위는 매우 넓다. 그렇기에 string 타입으로 변수를 선언하려 한다면 혹시 그보다 더 좁은 타입이 적절하지는 않을 지 검토해야한다.
아래의 예시를 보자
interface Album { artist: string; title: string; releaseDate: string; // YYYY-MM-DD recodingType: string; // "live" or "studio" }
string 타입이 남발된 모습이다. 게다가 주석에 타입 정보를 적어둔 걸 보면 인터페이스가 잘못 되었다는 것을 알 수 있다.
다음 예시처럼 엉뚱한 값을 설정할 수 있다.
const newAlbum: Album = { artist: 'Miles Davis', title: 'Kind of Blue', releaseDate: 'August 17th, 1959', //날짜의 형식이 다릅니다. recodingType: 'Studio', //오타 (대문자 S) }; //pass
type RecodingType = 'studio' | 'live'; interface Album = { artist: string; title:string; releaseDate: Date; recodingType: RecodingType; } const newAlbum: Album = { artist: 'Miles Davis', title: 'Kind of Blue', releaseDate: 'August 17th, 1959', //날짜의 형식이 다릅니다. recodingType: 'Studio', //오타 (대문자 S) }; //에러
keyof T 제네릭 사용하기
객체 배열의 하나의 속성을 가져오는 함수를 작성해보자.
function pluck<T, K exteds keyof T>(record: T[], key:K) { return records.map(r => r[key]) } pluck(albums, 'releaseDate') // type Date[]; pluck(albums, 'rr') // error 'rr'이 albums에 없음
keyof는 T타입의 모든 속성명을 타입으로 가지게 하며 위처럼 작성했을 때 별도의 제네릭을 추가하여 사용하지 않더라고 타입추론이 굉장히 쉽게 된다.
요약
- '문자열을 남발하여 선언된' 코드를 피하자. 모든 문자열을 할당할 수 있는 string 타입 보다는 더 구체적인 타입을 사용하는 것이 좋다.
- 변수의 범위를 보다 정확하게 표현하고 싶다면 string 타입 보다는 문자열 리터럴 타입의 유니온을 사용하면 된다.
이는 타입 체크를 더 엄격하게 할 수 있고 생산성을 향상시킬 수 있다. - 객체의 속성 이름은 함수 매개변수로 받을 때는 string보다 keyof T를 사용하는 것이 좋다.
'TypeScript' 카테고리의 다른 글
이펙티브 타입스크립트- 4장 타입설계 (1) (0) 2022.04.24 이펙티브 타입스크립트- 3장 타입추론 (5) (0) 2022.04.23 이펙티브 타입스크립트- 3장 타입추론 (4) (0) 2022.04.18 이펙티브 타입스크립트- 3장 타입추론 (3) (0) 2022.04.17 이펙티브 타입스크립트- 3장 타입추론 (2) (0) 2022.04.10