TS스터디 이펙티브 item31~32

온호성·2023년 4월 14일
0

🥶item 30 타입 주변에 null 값 배치하기

한 범위안의 변수가 null인 경우와 그렇지 않은 경우보다, 모두가 null이거나 전부 null이 아닌 경우로 분명히 구분하는것이 쉽다.

타입에 null을 추가하는 방식으로 이런 경우를 모델링 할 수 있는데

// 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]
}

-> 코드의 문제점은 min의 값만 null 체크를 하기 때문에 max 부분에서 number | undefined 형식의 인수가 number에 할당될 수 없다고 나온다.

// tsConfig: {"strictNullChecks":true} 

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) 
      						//~~'number | undefined' 형식의 인수는
      						//'number' 형식의 매개변수에 할당될 수 없다.
    }
  }
  return [min, max]
}

-> strictNullChecks 설정을 켜면 문제점은 extent의 반환값이 (number | undefined)[]로 추론되어 설계적 결함이 분명해져 extent를 호출하는 곳마다 타입 오류가 발생한다.

const [min, max] = extent([0, 1, 2])
const span = max - min
// ~~~   ~~~ Object is possibly 'undefined'

extent 함수의 오류는 undefined를 min에서만 제외했고 max에서는 제외하지 않았기 때문에 발생한 오류다.

두 개의 변수는 동시에 초기화되지만, 이런 정보는 타입 시스템에서 표현할 수 없다. max에 대한 체크를 추가해 오류를 해결할 수도 있지만 버그가 더 늘어날 것이다.

문제를 해결하기 위해 단일 객체 사용하고, 함수호출의 결과값을 단언 혹은 if 구문으로 체크하면 된다.

const 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;
  };

null을 섞어서 사용하는 경우가 클래스에서 문제가 될수 있는데, 값이 완전히 준비되지 않은 상태에서 네트워크 요청을 하는 경우 그 사이의 속성들의 값이 null이냐 아니냐에 따라서 불확실성이 생긴다.

-요약-

  • 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하면 안된다.
  • API 작성 시에는 반환 타입을 큰 객체로 만들고, 반환 타입 전체가 null이거나 null이 아니게 만들어야한다.
  • 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.
  • strictNullChecks 를 설정하면 코드에 많은 오류가 표시되지만, null과 관련된 문제점을 찾아낼 수 있기 때문에 반드시 필요하다.

🦍item 31 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

ts에서 유니온 타입(Union Type)은 여러 타입 중 하나를 나타내는 타입으로, | 연산자를 사용하여 표현된다. 예를 들어, 다음과 같이 선언할 수 있다.

type MyType = number | string;

이렇게 선언된 MyType은 숫자 또는 문자열 타입 중 하나를 나타낸다.

반면에 인터페이스(Interface)는 객체의 타입을 정의하는데 사용된다. 예를 들어, 다음과 같이 선언할 수 있다.

interface Person {
  name: string;
  age: number;
}

이렇게 선언된 Person 인터페이스는 이름과 나이라는 두 개의 속성을 가진 객체의 타입을 정의한다.

따라서, 인터페이스의 유니온을 사용하면 다양한 속성을 가진 객체의 타입을 정의할 수 있다. 예를 들어, 다음과 같이 선언할 수 있습니다.

interface Shape {
  kind: Square | Rectangle | Circle;
}

요롷게 유니온의 인터페이스로 작성하는 것 보다

interface Square {
  kind: "square";
  size: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

interface Circle {
  kind: "circle";
  radius: number;
}

type Shape = Square | Rectangle | Circle;

이렇게 선언된 Shape 타입은 Square, Rectangle, Circle 인터페이스의 유니온으로 정의하는게 잘못된 조합으로 섞이는 경우를 방지할 수 있다. 또한 안에 태그된 유니온이 있으면 태그를 참고하여 Shape의 타입의 범위를 좁힐 수도 있다.

이를 통해, 각각의 도형에 해당하는 속성을 가진 객체를 만들 수 있는데

const square: Shape = { kind: "square", size: 10 };
const rectangle: Shape = { kind: "rectangle", width: 10, height: 20 };
const circle: Shape = { kind: "circle", radius: 5 };

따라서, 인터페이스의 유니온을 사용하면 코드의 가독성을 높이고, 타입 안정성을 유지하면서 다양한 타입을 정의할 수 있다.

선택적 필드가 여러개 있는 경우엔 두 개의 속성을 하나의 객체로 모아 그 객체가 존재하는지 체크하는 방식으로 사용하면 편리하다. 또한 타입의 구조를 손 댈 수 없는 API의 결과 상황에서 인터페이스의 유니온을 사용해 관계를 모델링할 수 있다.

-요약-

  • 유니온 타입의 속성을 여러 개 가지는 인터페이스에서는 속성 간의 관계가 분명하지 않기 때문에 주의해야 한다.
  • 유니온 인터페이스보다 인터페이스의 유니온이 더 정확하다.
  • 타입스크립트가 제어 흐름을 분석할 수 있도록 타입에 태그를 넣는 것이 좋다. 태그된 유니온은 타입스크립트와 매우 잘 맞는 패턴이기 때문이다.

0개의 댓글

관련 채용 정보