ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 타입스크립트- 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[]로 추론되지만 버그와 함께 설계적 결함이 있다.

    1. min이 없을 경우 max에도 number로 추가해주었지만 max는 undefined로 추론된다.
    2. num이 0인 경우 if문에서 0은 false로 반환되므로 값이 덮어 씌여지는 현상이 발생한다.
    3. 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를 사용하는 것이 좋다. 

    댓글

Designed by Tistory.