아이템 28 유효한 상태만 표현하는 타입을 지향하기
효과적으로 타입을 설계하려면 유효한 상태만 표현할 수 있는 타입을 만드는 것이 중요하다.
유효하지 않은 상태
- A,B가 있다고 했을 때, 상태 값의 두 가지 속성이 동시에 정보가 부족하다. (A가 성공인지 실패인지 알 수 없다.)
- A,B의 두 가지 속성이 충돌한다. (A이면서 B인 상태가 있다.)
이런 무효한 상태가 존재하면 두 가지 모두를 제대로 구현할 수 없게 된다. 유효한 상태를 구현하려면 아래와 같이 명시적으로 모델링하는 태그된 유니온(또는 구별된 유니온)
을 사용해서 나타낸다. 이렇게 작성하면 코드가 길어지고 작성하기 어렵지만 무효한 상태를 허용하지 않도록 개선할 수 있다.
1 | interface RequestPending { |
타입을 설계할 때 어떤 값들을 포함하고, 어떤 값들을 제외할지 신중하게 생각하도록 한다. 유효한 상태를 표현하는 값만 허용하면 코드를 작성하기 쉬워지고 타입 체크가 용이해진다.
아이템 29 사용할 때는 너그럽게, 생성할 때는 엄격하게
함수의 시그니처는 당신의 작업은 엄격하게, 다른 사람의 작업은 너그럽게 받아들인다
는 일반적인 원칙을 따라야 한다. 즉 함수의 매개변수는 타입의 범위가 넓어도 되지만 결과를 반환할 때는 타입의 범위가 더 구체적이어야 한다.
- viewportForBounds의 타입의 선언이 만들어지고 사용될 때처럼 너무 자유로우면 오류가 발생하기 쉽다.
- 너무 자유롭다는 것은 수많은 선택적 속성(?)을 가지는 반환 타입을 가지고 있거나 유니온 타입을 사용하는 경우를 말한다.
- 유니온 타입의 요소별 분기를 위한 방법: 좌표를 위한 기본 형식을 구분한다.
- e.g. 배열과 배열 같은 것: LngLat, LngLatLike
- 완전하게 정의된 버전과 부분적으로 정의된 부분을 구분한다.
- 완전하게 정의된 버전: Camera
- 부분적으로 정의된 버전: Camera option
1 | type Feature = any; |
아이템 30 문서에 타입 정보를 쓰지 않기
코드에 대한 정보가 주석으로 남아있을 때 그 주석의 정보와 코드가 맞이 않을 때가 있다. 타입스크립트의 타입 구문 시스템은 간결하고 구체적이며, 쉽게 읽을 수 있도록 설계되었기 때문에 코드에 대한 설명 및 타입 정보를 주석으로 남기기 보다 타입스크립트의 타입 구문을 사용하도록 한다. 타입 구문은 타입스크립트 컴파일러가 체크해 주기 때문에 구현체와의 정합성이 어긋나지도 않는다. 또 주석은 누군가 고치기 전에 강제로 동기화 되지 않는다. 타입스크립트는 타입 체커가 타입 정보를 동기화하도록 강제한다.
타입스크립트는 명시적으로 사용하는 것이 좋다. ageNum이라는 변수를 선언하는 것보다 age로 변수 선언 후 타입은 num임을 명시하는 것이 더 좋다. 단 단위가 있는 숫자들은 단위가 무엇인지 확실하지 않다면 변수명 또는 속성 이름에 단위를 포한한다. 예를 들어 temperature보다 temperatureC가 훨씬 명확하다.
아이템 31 타입 주변에 null 값 배치하기
어떤 변수가 null인지 아닌지를 분명히 해야한다. null과 null이 아닌 값을 섞어서 사용하면 문제가 생긴다.
1 | function extent(nums: number[]) { |
위 예제에서 extent의 반환값이 (number | undefined)[]로 추론된다. 이렇게 되면 extent를 호출하는 곳마다 타입 오류의 형태로 나타난다. 더 나은 해법으로는 min, max를 한 객체 안에 넣고 null이거나 null이 아니게 하는 아래와 같은 방법으로 작성한다.
1 | function extent(nums: number[]) { |
TL;DR
- 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하면 안된다.
- API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null이거나 null이 아니게 만들어야 한다.
- 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 한다.
- strictNullChecks를 설정하면 코드에 많은 오류가 표시되갰지만, null 값과 관련된 문제점을 찾아낼 수 있기 때문에 반드시 필요하다.
아이템 32 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
유니온 타입의 속성을 가지는 인터페이스를 작성 중이라면, 인터페이스의 유니온 타입을 사용하는 게 더 알맞지 않을지 검토해보아야 한다.
1 | interface Layer { |
위의 예제는 벡터를 그리는 프로그램을 작성 중이고, 특정한 기하학적 타입을 가지는 계층의 인터페이스를 정의한다고 가정하는 코드이다. 그런데 이 코드에서 layout이 LineLayout(직선)이면서 paint 속성이 FillPaint타입이 되는 조합은 성립하지 않는다. 그런데 위의 코드는 그런 조합을 허용하는 코드이기 때문에 오류가 발생하기 쉽다. 이런 상황에서는 layout과 paint 속성이 지금처럼 잘못된 조합으로 섞이지 않도록 아래와 같이 각각 타입의 계층을 분리된 인터페이스로 작성하도록 한다.
1 | interface FillLayer { |
이렇게 작성하면 유효한 상태만을 표현하게 된다.
1 | interface Layer { |
위와 같은 태그된 유니온에서도 유효하지 못한 상태가 섞이는 문제가 발생한다. 태그된 유니온은 여러개의 타입을 유니온으로 선언할 때 각 타입에 태그가 있어서 이것으로 구분하는 것을 말한다. 여기서 Layer 속성 중 하나는 문자열 타입의 유니온인데, 이 역시 type: ‘fill’일 때 LineLayout과 PointPaint 타입이 함께 쓰이는 것은 유효하지 않다. 그래서 아래와 같이 Layer의 인터페이스를 유니온으로 변환하는 방식으로 개선할 수 있다.
1 | interface FillLayer { |
이와 같이 어떤 데이터 타입을 태그된 유니온
으로 표현할 수 있다면 그렇게 하는 것이 좋다. 특히 여러 개의 선택적 필드가 동시에 값이 있거나 동시에 undefined일 때 이 패턴이 적절하다.
1 | interface Person { |
위와 같이 주석으로 타입에 대한 정보를 남기 코드가 있다고 했을 때, 이 타입 정보는 placeOfBirth와 dateOfBirth의 관계가 정확하게 표현되어 있지 않다. 이런 경우에는 아래와 같이 타입의 구조를 변경하여 두 개의 속성을 하나의 객체로 모으는 것이 더 나은 설계이다. 이는 null 값을 경계로 두는 아이템 31의 방법과 비슷하다.
1 | interface Person { |
하지만 타입의 구조를 손 댈 수 없는 경우라면(e.g.API response) 아래와 같이 인터페이스의 유니온을 사용해서 속성 사이의 관계를 모델링한다.
1 | interface Person { |
TL;DR
- 유니온 타입의 속성을 여러 개 가지는 인터페이스에서는 속성 간의 관계가 분명하지 않기 때문에 실수가 자주 발생하므로 주의하도록 한다.
- 유니온의 인터페이스보다 인터페이스의 유니온이 더 정확하고 타입스크립트가 이해하기 좋다.
- 타입스크립트가 제어된 흐름을 분석할 수 있도록 타입에 태그를 넣는 것을 고려하도록 한다. 태그된 유니온은 타입스트립트와 매우 잘 맞아서 자주 볼 수 있는 패턴이다.
아이템 33 string 타입보다 더 구체적인 타입 사용하기
string 타입의 범위는 매우 넓어서 ‘x’와 같은 한 글자와 , ‘call me ishmael….’로 시작하는 모비딕의 전체 내용과 같이 긴 텍스트도 string 타입이다. 그러므로 타입으로 변수를 선언 할 때, 더 좁은 타입이 적절하지 않을 지 검토해보아야 한다. string은 any와 비슷한 문제를 가지고 있어서 잘못 사용하면 무효한 값을 허용하고, 타입 간의 관계를 감추어 버리기 때문이다.
1 | interface Album { |
위의 Album은 string이 남발되어 타입이 모델링 되었다.(stringly typed) 이 경우 releaseDate, recordinType이 Album에 정의된 주석과 다른 형태로 모델링되었지만 string이기 때문에 타입 체커를 통과한다. 아래와 같이 함수 호출시 매개변수의 순서가 바뀐 경우에도 둘 다 문자열이기 때문에 타입 체커를 통과한다.
1 | function recordRelease(title: string, date: string) { |
이런 경우 아래와 같이 타입을 좁히는 방식으로 개선할 수 있다. releaseDate는 날짜형식으로 제한하고, recordingType은 두 개의 값을 가진 유니온 타입으로 정의할 수 있다.
1 | type RecordingType = 'studio' | 'live'; |
이렇게 작성하면 앞서 오류가 발생할 상황에서도 타입체커를 통과했던 것과 다르게 타입스크립트가 타입 체커를 세밀하게 체크할 수 있어서 타입 오류를 잘 검사하게 된다.
string 타입 좁히기의 장점
- 타입을 명시적으로 정의하여 다른 곳으로 값이 전달되어도 타입 정보가 유지된다.
1 | //recordingType 매개변수에 대한 타입을 RecordingType과 같이 사용하면 편집기에서 자동완성을 통해 타입에 대한 설명을 확인할 수 있다. |
- 타입을 명시적으로 정의히고, 해당 타입의 의미를 설명하는 주석을 함께 쓸 수 있다.
1 | /** What type of environment was this recording made in? */ |
- keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능하다.
- underscore 라이브러리의 pluck함수의 시그니처를 작성한다.
1 | function pluck(record: any[], key: string): any[] { |
- 2.타입 체크가 되지만 정밀하지 못하다. 반환값에 any를 쓰는 것도 바람직 하지 않다.
1 | //제너릭 타입을 도입하여 개선한다.하지만 매개변수가 string이므로 오류를 발생시킨다. |
- 3.매개변수로 Album의 배열을 전달했기 때문에 string이었던 타입이 type k와 같이 좁혀졌다.
1 | type RecordingType = 'studio' | 'live'; |
- keyof T의 부분 집합(아마도 단일값)으로 두 번째 제너릭 매개변수를 도입한다.
1 | //T[key of]T는 T 객체 내에 가능한 모든 값의 타입이라서 string보다는 범위가 좁지만 여전히 넓다. |
아래와 같이 두 번째 제너릭 매개변수를 도입한다.
1 | function pluck<T, K extends keyof T>(record: T[], key: K): T[K][] { |
아이템 34 부정확한 타입보다는 미완성 타입을 사용하기
타입이 구체적일수록 버그를 더 잘 잡고, 타입스크립트가 제공하는 도구를 활용할 수 있는데, 잘못된 타입은 차라리 타입이 없는 것만 못하기 때문에 주의를 기울여야 한다.
References
[이펙티브 타입스크립트] 댄 밴더캄 지음