ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 타입스크립트- 4장 타입설계 (1)
    TypeScript 2022. 4. 24. 22:10

    아이템 28. 유효한 상태만 표현하는 타입을 지향하기 

    타입을 잘 설계하면 코드를 직관적으로 작성할 수 있는 반면 엉망이라면 어떠한 기억이나 문서도 도움이 되지 못한다.

    아래의 예시를 통해 잘못된 타입 설계를 알아보자.

    예시는 페이지를 선택하면 페이지의 내용을 로드하고 화면에 표시한다. 

    interface State {
        pageText: string;
        isLoading: boolean;
        error?: string;
    }
    
    //페이지 그리는 함수
    function renderPage(state: State) {
      if(state.error) {
        return `Error!`
      } else if (state. isLoading) {
        return `Loading`
      }
      return `Render`
    }
    
    // 위 코드의 문제점은 ? 
    
    분기조건이 명확히 분리되지 않다는 것이다. isLoading이 true이면서 동시에 error값이 존재하면 
    로딩 중인 상태인지 오류가 발생한 상태인지 명확히 구분할 수 없다.
    
    
    //페이지 전환 함수 
    async function changePage(state:State, newPage: string) {
        state.isLoading = true;
        try {
            const response = await fetch(getUrlForPage(newPage));
            if (!response.ok) {
                thorw new Error(`Error`);
            }
            const text = await response.text();
            state.isLoading = false;
            state.pageText = text;
        } catch (e) {
            state.error = '' + e;
        }
    }

    위 코드의 문제점은 

    1. error 발생 시 isLoading을 false 처리 해주지 않는다.
    2. error를 초기화 해주지 않아 지속적으로 error가 남아 있게된다.
    3. 로딩 중 페이지 전환 시 예상하기 어렵다. 응답 순서에 따라 보여지는게 달라진다.
    4. error와 loading 속성이 모두 충돌(오류 이면서 동시에 로딩 중)할 수 있다.  
    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 };
    }

    여기서는 요청 과정의 각각의 상태를 명시적으로 모델링하는 태그된 유니온을 사용했다.

    코드의 길이가 길어졌지만 무효한 상태를 허용하지 않도록 크게 개선됐다.

    function renderPage(state: State) {
      const { currentPage } = state;
      const requestState = state.requests[currentPage];
      switch (requestState.state) {
        case 'pending':
          return `Loading`;
        case 'error':
          return `Error`;
        case 'ok':
          return `Render`;
      }
    }
    
    async function changePage(state: State, newPage: string) {
      state.requests[newPage] = { state: 'pending' };
      state.currentPage = newPage;
      try {
        const response = await fetch(getUrlForPage(newPage));
        if (!response.ok) {
          throw new Error(`Error`);
        }
        const pageText = await response.text();
        state.requests[newPage] = { state: 'ok', pageText };
      } catch (e) {
        state.requests[newPage] = { state: 'error', error: '' + e };
      }
    }

    loading, error 상태의 모호함이 사라지고 별도의 초기화를 안해주어도 state에 따라 변경되기 때문에 현재 페이지는 명확하다.

    요청이 진행중인 상태에서도 페이지를 변환해도 요청이 실행되지만 이미 pending 상태이기 때문에 ui에는 영향을 미치지 않는다.


     

    요약 

    • 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란과 오류를 유발한다.
    • 유효한 상태만 표현하는 타입을 지향해야한다. 코드가 길어지거나 표현하기 어렵지만 결국 시간을 절약하고 
      고통을 줄일 수 있따. 

    아이템 29. 사용할 때는 너그럽게, 생성할 때는 엄격하게 

    존포스텔의 "당신의 작업은 엄격하게 하고, 다른 사람의 작업은 너그럽게 받아들여야 한다. " 견고성 원칙은

    함수의 시그니처에도 비슷하게 적용된다.  

     

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

    아래의 예시는 카메리의 위치를 조정하고 경계 박스의 뷰포트를 계산하는 방법을 제공하는 3D 매핑 API이다.

    declare function setCamera(camera: CameraOptions): void;
    declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;
    
    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];
    
    //여기서 LngLat, LngLatBounds은 굉장히 넓은 타입으로 설정되었다.
    --------------------------------------------
    const camera = viewportForBounds(LngLatBounds)
    // camera 타입은 CameraOptions으로 되지만 타입안의 속성은 모두 optional이다.
    const {center: {lat,lng}, zoom} = camera;
    // Property 'lat | 'lng'' does not exist on type 'LngLat | undefined'.
    zoom // type: number | undefined
    

     수 많은 선택적 속성을 가지는 반환 타입과 유니온 타입은 viewportForBounds를 사용하기 어렵게 만든다.

    매개변수 타입의 범위가 넓으면 사용하기 편리하지만, 반환 타입의 범위가 넓으면 불편하다. 

    즉 사용하기 편리하면 API일수록 반환 타입이 엄격하다 

    omit, partial 같은 타입변환을 사용해 좀 더 유연하게 작성할 수 있다. 

    interface LngLat {lng: number; lat:number};
    type LngLagLike = LngLat | {lon: number; lat: number} |    [number, number];
    
    interface Camera {
      center: LngLat;
      zoom: number;
      bearing: number;
      pitch: number;
    }
    interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
      center? : LngLatLike;
    }
    type LngLatBounds =
    {northeast: LngLagLike, southwest: LngLagLike} |
    [LngLagLike, LngLagLike] |
    [number, number, number, number];
    
    declare function setCamera(camera: CameraOptions): void;
    declare function viewportForBounds(bounds: LngLatBounds): Camera;
    
    
    --------------------------------------------
    const camera = viewportForBounds(LngLatBounds)
    const {center: {lat,lng}, zoom} = camera // pass
    zoom // type: number

    반환타입을 명시적으로 사용해 더욱 깔끔해졌다.

     

    보충

    1. Partial

    파셜 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.

    interface Address {
      email: string;
      address: string;
    }
    
    type MyEmail = Partial<Address>;
    const me: MyEmail = {}; // 가능
    const you: MyEmail = { email: "noh5524@gmail.com" }; // 가능
    const all: MyEmail = { email: "noh5524@gmail.com", address: "secho" }; // 가능
    
    interface Product {
      id: number;
      name: string;
      price: number;
      brand: string;
      stock: number;
    }
    
    // Partial - 상품의 정보를 업데이트 (put) 함수 -> id, name 등등 어떤 것이든 인자로 들어올수있다
    // 인자에 type으로 Product를 넣으면 모든 정보를 다 넣어야함
    // 그게 싫으면
    interface UpdateProduct {
      id?: number;
      name?: string;
      price?: number;
      brand?: string;
      stock?: number;
    }
    // 위와 같이 정의한다.
    // 그러나 같은 인터페이스를 또 정의하는 멍청한 짓을 피하기 위해서 우리는 Partial을 쓴다.
    function updateProductItem(prodictItem: Partial<Product>) {
      // Partial<Product>이 타입은 UpdateProduct 타입과 동일하다
    }
    

    2. Pick

    픽 타입은 특정 타입에서 몇 개의 속성을 선택하여 타입을 정의한다.

    interface Product {
      id: number;
      name: string;
      price: number;
      brand: string;
      stock: number;
    }
    
    // 상품 목록을 받아오기 위한 api
    function fetchProduct(): Promise<Product[]> {
      // ... id, name, price, brand, stock 모두를 써야함
    }
    
    type shoppingItem = Pick<Product, "id" | "name" | "price">;

    3. Omit

    특정 속성만 제거한 타입을 정의합니다. pick의 반대

    interface Product {
      id: number;
      name: string;
      price: number;
      brand: string;
      stock: number;
    }
    
    type shoppingItem = Omit<Product, "stock">;
    
    const apple: Omit<Product, "stock"> = {
      id: 1,
      name: "red apple",
      price: 1000,
      brand: "del"
    };
    

     

    예시출처 : https://kyounghwan01.github.io/blog/TS/fundamentals/utility-types/#omit

     


     

    요약 

    • 보통 매개변수 타입은 반환 타입에 비해 범위가 넓은 경향이 있다. 선택적 속성과 유니온 타입은 반환 타입보다 매개변수 타입에 더 일반적이다.
    • 매개변수와 반환 타입의 재사용을 위해서 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 도입하는 것이 좋다.

    아이템 30. 문서에 타입 정보를 쓰지 않기 

    /**
     * 전경색(foreground) 문자열을 반환합니다.
     * 0 개 또는 1개의 매개변수를 받습니다.
     * 매개변수가 없을 때는 표준 전경색을 반환합니다.
     * 매개변수가 있들 때는 특정 페이지의 전경색을 반환합니다.
     **/
     
    function getForegroundcolor(page?: string) {
      return page === 'login' ? { r: 127, g: 127, b: 127 } : { r: 0, g: 0, b: 0 };
    }

    위의 예시를 보자. 코드와 주석 정보가 맞지 않다.

    타입스크립트의 타입 구문 시스템은 간결하고 구체적이며 쉽게 읽을 수 있도록 설계 되어 있다.

    함수의 입력과 출력의 타입을 코드로 표현하는 것이 주석보다 더 나은 방법이다. 

    누군가 강제하지 않는 이상 주석은 코드와 동기화되지 않는 반면, 타입 구문은 타입스크립트 타입체커가 타입 정보를 동기화 하도록 강제한다. 

    위의 코드는 다음과 같이 개선 할 수 있다. 

    /** 애플리케이션 또는 특정 페이지의 전경색을 가져옵니다.*/
    function getForegroundcolor(page?: string): Color {
      //....
    }

    또한 아래와 같이 매개변수를 변경하지 않는다는 주석도 좋지 않다.

    /** nums를 변경하지 않습니다. */
    function sort(nums: number[]) {}

    위와 같은 코드는 주석보다 readonly number[]형태로 작성하여 주석을 쓰지 않는 편이 좀더 효율적이다.


     

    요약 

    • 주석과 변수명에 타입 정보를 적는 것을 피해야 한다. 최악의 경우 타입 정보에 모순이 발생한다.
    • 타입이 명확하지 않은 경우 변수명에 단위 정보를 포함하는 것을 고려하는 것이 좋다. 

     

    댓글

Designed by Tistory.