이펙티브 타입스크립트- 4장 타입설계

fullth·2022년 7월 14일
0

Effective TypeScript

목록 보기
4/6

아이템 28 유효한 상태만 표현하는 타입을 지향하기(valid-states)

타입을 잘 설계 -> 직관적인 코드를 작성할 수 있음.

효과적인 타입설계 -> 유효한 상태만 표현할 수 있는 타입을 만들어 내는 것.

아래 코드는 애플리케이션의 상태를 제대로 표현한 방법.

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 사용할 때는 너그럽게, 생성할 때는 엄격하게(loose-accept-strict-produce)

해당 아이템의 이름은 TCP와 관련해서 존 포스텔이 쓴 견고성 원칙 또는 포스텔의 법칙에서 따온 것이라 함.

TCP 구현체는 견고성의 일반적 원칙을 따라야 한다.
당신의 작업은 원격하게 하고, 다른 사람의 작업은 너그럽게 받아들여야 한다.

함수의 시그니처에도 비슷한 규칙을 적용해야 함.

함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과를 반환할 때는 타입의 범위가 더 구체적이여야 함.

아래의 예시 코드는  3D 매핑 API. 카메라의 위치를 지정하고, 경계 박스의 뷰포트를 계산하는 방법을 제공함.

// 일부 값은 건드리지 않으면서 동시에 다른 값을 설정할 수 있어야 함.
// 즉, 모든 필드는 선택적.
interface CameraOptions {
  center?: LngLat;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}

// 함수 호출을 쉽게 할 수 있도록 편의성 제공.
type LngLat =
  { lng: number; lat: number; } |
  { lon: number; lat: number; } |
  [number, number];
  
type LngLatBounds =
  {northeast: LngLat, southwest: LngLat} |
  [LngLat, LngLat] |
  [number, number, number, number];
  
declare function setCamera(camera: CameraOptions): void;

declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;

아이템 30 문서에 타입 정보를 쓰지 않기(jsdoc-repeat)

실제 타입 정보와 다를 수 있는 상황을 방지해서, 주석에 변수명과 타입 정보를 적는 것을 피해라.

타입이 명확하지 않은 경우 변수명에 단위 정보를 포함하는 것을 고려하는 것이 좋다. ex.) timeMs or tempertureC

아이템 31 타입 주변에 null 값 배치하기(null-values-to-perimeter)

변수 A와 B가 있을 때, B가 A의 값으로부터 비롯되는 값이라면, A가 null이 될 수 없을 때 B역시 null이 될 수 없고,

반대의 경우도 성립한다.

이 관계는 겉으로 드러나지 않아서 혼란을 유발한다.

값이 전부 null이거나 전부 null이 아닌 경우로 구분되면 다루기 쉽다.

아래 예시 코드는 최솟값과 최댓값을 구하는 함수 extent이다.

// tsConfig: {"strictNullChecks":false}

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]를 반환함.

strictNullChecks 설정을 키면, 두 문제에 대한 설계적 결함이 드러남.

function extent(nums: number[]) {
  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;
}

아이템 32 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기(union-of-interfaces)

유니온 타입의 속성을 가지는 인터페이스를 작성 중이라면, 인터페이스의 유니온 타입 사용이 더 알맞지 않을지 고려해라.

type FillPaint = unknown;
type LinePaint = unknown;
type PointPaint = unknown;
type FillLayout = unknown;
type LineLayout = unknown;
type PointLayout = unknown;

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

위의 코드는 layout은 FillLayout이면서 paint는 LinePaint인 조합을 허용한다.

의도하지 않았을 경우 오류를 발생하기 쉽상이고, 인터페이스를 다루기 어려워진다.

type FillPaint = unknown;
type LinePaint = unknown;
type PointPaint = unknown;
type FillLayout = unknown;
type LineLayout = unknown;
type PointLayout = unknown;

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 타입보다 더 구체적인 타입 사용하기(avoid-strings)

string 타입의 범위는 너무 넓음.

"A", "Lorem Ipsum is simply dummy text of the printing and typesetting industry."

위 문자열 둘 다 string 타입임.

interface Album {
  artist: string;
  title: string;
  releaseDate: string;  // YYYY-MM-DD
  recordingType: string;  // E.g., "live" or "studio"
}

만약 타입을 string으로 타입을 정의하려면, 더 좁은 타입이 적합하진 않은지 검토할 필요가 있음.

위의 주석 달린 두 타입에 날짜 타입을 다르게 입력해도, 대문자로 입력해도 타입 체커에는 통과됨.

type RecordingType = 'studio' | 'live';

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

위와 같은 방법으로 타입을 좁힐 수 있고, RecordingType에 enum을 사용하지 않는 이유는 아이템 58에서 설명한다고 함.

위와 같은 방식에는 3가지 장점이 있음.

1. 다른 곳에 전달되었을 때도 타입 정보가 유지된다는 것.

2. RecordingType에 /* 어디서 녹음되었는지 */ 와 같은 주석을 달 수 있음. 

3. keyof 연산자로 세밀하게 객체의 속성 체크가 가능해짐. 

-> 3번은 차후에 refactoring하는 과정에서 유용하게 쓰일 것 같음.

아이템 34 부정확한 타입보다는 미완성 타입을 사용하기(incomplete-over-innacurate)

타입이 없는 것보다 잘못된게 더 나쁘다.

타입이 구체적으로 정제된다고 해서 무조건 정확도가 올라가진 않는다. (any 정제가 무조건 정확도가 올라가는 것이 아님.)

-> react 코드에서 비슷한 경험을 했음. 타입을 구체화 할수록 본문의 내용처럼 에러 표시가 장황해지고, 더 복잡해짐.

더 나은 방안은 아직 모르겠음.

profile
Web Backend Developer

0개의 댓글