본문 바로가기
Book Review/Effective Typescript

Day10_Effective Typescript 29~31

by 예 강 2023. 6. 16.

2022년 12월 28일 글입니다.

 

ITEM 29. 사용할 때는 너그럽게, 생성할 때는 엄격하게 작성하라

요약 : 매개변수는 너그럽게 받고, 반환값은 엄격하게 반환하라

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;
  1. 위와 같은 카메라에 대한 타입이 있다. 카메라 설정은 일부 값을 건드리지 않으면서 다른 값을 받을 수 있도록 CameraOptions는 모두 선택적이다.
  2. 카메라의 경계박스의 뷰포트를 계산하는 viewportForBounds는 bounds 값을 받아 카메라 옵션을 반환한다.
  3. LngLat, LngLatBounds 모두 union을 이용해 타입을 넓힘으로써 다양한 매개변수를 받고있다. 즉 사용자가 사용하기에 너그럽게 생성했다.

  1. 너그러움의 결과는 에러로 반환된다. 반환타입의 범위가 너무 넓어서 타입체커가 제대로 예측하지 못한것 같다. camera를 안전한 타입으로 사용하려면 유니온 타입의 각 요소별로 코드를 분기해야 한다.
interface LngLat { lng: number; lat: number; };
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number];

interface Camera {
  center: LngLat;
  zoom: number;
  bearing: number;
  pitch: number;
}

//omit은 ~를 제외하고 부분적으로 받는다는 뜻이다.  즉 camera의 center를 제거한 속성들만 partial로 받겠다는 뜻
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
  center?: LngLatLike;
}
type LngLatBounds =
  {northeast: LngLatLike, southwest: LngLatLike} |
  [LngLatLike, LngLatLike] |
  [number, number, number, number];

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
  1. 위 코드를 보면 center부분을 분기로 처리하여 Camera의 center를 받아오지 않는다.
    CameraOptions에서 center는 LngLatLike로 재정의 된다. LatLngLike로 재정의된 곳엔 LngLat으로 타입이 정의되어있으므로 디스트럭팅으로 {lng,lat}을 받아올 수 있기 때문에 오류가 나지 않는 것 같다.

코드가 중복되지만 이런식으로 명시적 작성을 해줘도 된다는데, 위 코드가 더 멋있는 것 같다.


ITEM 30. 문서에 타입정보를 쓰지 안기

요약 1 : 주석으로 설명하기 보다는, 코드로 한눈에 딱 알아볼 수 있게 작성하기
=> 주석은 나중에 유지보수하기 귀찮기도 하고 자동으로 변경되지도 않는다.
요약 2 : 특정 매개변수를 설명하고 싶다면 JSDoc의 @PARAM 구문을 사용하자

type Color = { r: number; g: number; b: number };
/**매개변수가 있을 때는 특정페이지의 전경색을 반환합니다..  보다는*/


function getForegroundColor(page?: string): Color {
  // COMPRESS
  return page === 'login' ? {r: 127, g: 127, b: 127} : {r: 0, g: 0, b: 0};
  // END
}

  1. 이처럼 JSDoc의 param을 이용하는게 더좋다. parameter에 댔을때 관련설명도 보여주는 좋은 기능이다.
  2. 정보의 모순이 발생할 수도 있기때문에 주석과 변수명에 타입정보를 적는건 피해야 한다.
    타입이 명확하지 않을 떈, 변수명에 단위 정보를 포함하는것을 고려해라
    (timeMs, temperatureC 등등)

ITEM 31. 타입주변에 null값 배치하기

요약 1 : null값을 잘 걸러줘라, undefined를 포함하게 하지 말아라!
요약 2 : 데이터를 받는 비동기함수의 경우 받을때는 null값을 허용하되 반환할 때는 null값이 아니게 반환하라
요약 3 : null값인 변수와 아닌 변수가 섞이게 하지 말아라, 골치아파진다.

interface UserInfo { name: string }
interface Post { post: string }
declare function fetchUser(userId: string): Promise<UserInfo>;
declare function fetchPostsForUser(userId: string): Promise<Post[]>;
class UserPosts {
  user: UserInfo | null;
  posts: Post[] | null;

  constructor() {
    this.user = null;
    this.posts = null;
  }

  async init(userId: string) {
    return Promise.all([
      async () => this.user = await fetchUser(userId),
      async () => this.posts = await fetchPostsForUser(userId)
    ]);
  }

  getUserName() {
    // ...?
  }
}
  1. 이 코드에서 user와 posts 는 null로 초기화 되어있다. Promise.all이 도는동안 둘다 null 이거나, 둘다 null이 아니거나 둘중 하나만 null 인 4가지 상태가 된다. 그러나 이런 불확실성이 클래스의 모든 메서드에 나쁜 영향을 끼친다고 한다. => null 체크가 난무하고 버그를 양산하게 됌
class UserPosts {
  user: UserInfo;
  posts: Post[];

  constructor(user: UserInfo, posts: Post[]) {
    this.user = user;
    this.posts = posts;
  }

  static async init(userId: string): Promise<UserPosts> {
    const [user, posts] = await Promise.all([
      fetchUser(userId),
      fetchPostsForUser(userId)
    ]);
    return new UserPosts(user, posts);
  }

  getUserName() {
    return this.user.name;
  }
}

개선한 코드, init을 호출하면 객체가 반환된다. (js엔 init 내장함수가 없다.)user와 posts가 초기화 된후 UserPosts가 반환된다. 위와 같이 코드를 짜면 null로 초기화 될 이유 없어서 더 좋다고 한다.