타입스크립트 스터디5회차

안윤경·2023년 1월 11일

타입설계

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

타입 설계를 명확하게 함으로써 예상치 못한 버그가 숨어들 여지를 줄일 수 있다.
유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래하기 쉽고 오류를 유발하게 됩니다.
즉 유효함 상태만 표현하는 타입을 지향해야 합니다.

예시)
State 타입을 예시로 들어보겠습니다.
State 타입에는 a와 b 속성은 반드시 가지고 c와 d 속성은 선택적인 속성입니다.
그리고 State 타입은 3가지 상태가 있다고 가정해보겠습니다. (One / Two / Three)

interface State {
  a: string;
  b: string;
  c?: string;
  d?: string;
}

위처럼 작성시 One / Two / Three 상태가 아닌 다른 상태를 만들어버릴 수 있습니다.

interface StateOne {
  a: string;
  b: string;
}

interface StateTwo {
  a: string;
  b: string;
  c: string;
}

interface StateThree {
  a: string;
  b: string;
  d: string;
}

type State = StateOne | StateTwo | StateThree;

처럼 상태를 먼저 정의하는 것이 좋습니다.

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

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

이를 방지하기 위해서 타입 선언시에는 명시적으로 엄격하게 선언하고,타입을 조건 완화 조합하여 느슨한 타입을 만들어 매개변수에 사용하는 것이 좋다.
반대로 타입을 반환할 때에는 반드시 엄격한 타입을 반환해야 사용성이 좋아진다.

보통 매개변수 타입은 반환 타입에 비해 범위가 넓은 경향이 있다.
선택적 속성과 유니온 타입은 반환 타입보다 매개변수 타입에 더 일반적이다.

매개변수와 반환 타입의 재사용을 위해서 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 도입하는 것이 좋다.

//??이해가 잘 안됨

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

타입스크립트의 타입 구문 시스템은 간결하고,구체적이며, 쉽게 읽을 수 있도록 설계가 되었습니다.
주석은 동기화가 되지 않지만 타입은 코드가 변경되면 정보를 동기화하도록 강제합니다
따라서 타입정보를 주석으로 쓰는 것보다 타입선언을 통해 타입을 관리하는 것이 훨씬 효율적입니다.

주석으로 써주지 않고도 변하지 않는 다는 것을 readonly로 표현할 수 있습니다.

function sort(nums: number[]) {/* ... */ }
...
function sort(nums: readonly number[]) {/* ... */}

또한 변수명에 타입정보를 쓰는 것은 좋지 않습니다

const ageNum = 10; // Bad

const age: number = 10; // Good 타입은 number

아이템 31. 타입 주변에 null 값 배치하기

strictNullChecks 속성을 사용하면 많은 오류가 나타나게 되며 null 값과 관련된 문제점을 찾아내주기 때문에 반드시 필요합니다.

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

const extent = (nums: number[]) => {
  let min;
  let 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의 값만 체크하기 때문에 max부분에서 number | unudefined 형식의 인수가 number에 할당될 수 없다고 나옵니다.

또한, [0, 1, 2]의 경우 0이 falsy값이므로 값이 덮어씌워지는 경우와 nums 배열이 빈 배열이라면, 둘 다 undefined를 반환합니다.

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

이런식으로 고쳐주면 반환타입이 [number, number] | null이라 좀 더 쉽게 사용이 가능합니다

아이템32. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

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

위에는 타입이 섞일 가능성이 있다.

// 이렇게 작성하자.(각각 타입의 계층을 분리된 인터페이스로 두기)
interface FillLayer {
  type: 'fill';
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  type: 'line'
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  type: 'paint'
  layout: PointLayout;
  paint: PointPaint;
}

type Layer = FillLayer | LineLayer | PointLayer;

이렇게 하면 잘못된 조합으로 섞이는 경우를 방지할 수 있다. 또한 안에 태그된 유니온이 있으면 태그를 참고하여 Layer의 타입의 범위를 좁힐 수도 있다.

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

아이템33. string 타입보다 더 구체적인 타입 사용하기

string타입의 범위는 매우 넓기 때문에 오류를 잡을 수 없을 수 도 있습니다. 즉 더 좁은 타입이 존재한다면 그 타입으로 써주는 것이 더 좋습니다.

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

// 이렇게 쓰자
type RecordingType = 'live' | 'studio';

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

위와 같이 바꾸면 3가지 장점

  • 타입을 명시적으로 정의함으로써 다른 곳으로 값이 전달되어도 타입 정보가 유지
  • 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙여넣을 수 있다.(편집기에서 확인 가능)
  • keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능해진다.

아이템34 부정확한 타입보다는 미완성 타입을 사용하기

타입선언의 정밀도를 높이려다가 더 좋지않은 개발 경험을 할 수 있으므로 이런 일을 할 때 주의해서 해야한다!

→ 오히려 타입이 부정확해지는 경우도 있다.

또한 타입선언의 정밀도를 높이려다가 정밀도를 높이지 않았을 때보다 발생하는 메시지의 오류가 부정확해지는 경우도 있다.

따라서 정확하게 타입을 모델링할 수 없다면 부정확하게 모델링하지 말고, any와 unknown을 구별해서 사용하자.

any: 어떠한 값이든 가능. 타입을 좁혀서 사용하지 않아도 된다.

unknown: 어떠한 값이 올 수 있는지 모르므로, 타입을 좁혀서 사용함. 다른값에 할당이 불가능하다.(any 타입 제외)

아이템35 데이터가 아닌, api와 명세를 보고 타입 만들기

API 명세가 있다면 명세로부터 타입을 생성하는 것이 좋다. (GraphQL과 상성이 좋음.)

특히, 특정 쿼리에 대해 타입스크립트의 타입을 생성할 수 있다는 점이 좋음

쿼리와 스키마가 바뀌면 타입도 자동으로 바뀐다. 타입은 GraphQL 스키마로부터 생성되기 때문에 타입과 실제 값이 항상 일치한다.

아이템36.해당 분야의 용어로 타입 이름 짓기

타입 이름을 짓는 것 또한 타입 설계에서 중요한 부분입니다.

같은 의미에 다른 이름을 붙이기보다, 특별한 의미가 있을 때만 용어를 구분해야 합니다.

자체적으로 용어를 만들어내기보다 해당 분야에 이미 존재하는 용어를 사용하는 것이 좋습니다.
이렇게 하면 타입의 명확성을 올릴 수 있다. 좋은 이름은 추상화의 수준을 높이고 의도치 않은 충돌의 위험성을 줄여줍니다.

아이템37.공식 명칭에는 상표를 붙이기

interface Vector2D {
  _brand: "2d";
  x: number;
  y: number;
}
function vec2D(x: number, y: number): Vector2D {
  return { x, y, _brand: "2d" };
}
function calculateNorm(p: Vector2D) {
  return math.sqrt(p.x * p.x + p.y * p.y);
}
calculateNorm(vec2D(3, 4)); // 정상, 5
const vec3D = { x: 3, y: 4, z: 1 };
calculateNorm(vec3D); // ❌ '_brand'속성이 ... 형식에 없습니다

상표(_brand)를 사용해서 calculateNorm 함수가 vector2D 타입만 받는 것을 보장한다.

타입스크립트는 구조적 타이핑을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있다.

값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 한다.

상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하느 것과 동일한 효과를 얻을 수 있다.

profile
프론트엔드 개발자 안윤경입니다

0개의 댓글