이펙티브 타입스크립트 : 좀 더 타입 지정 잘해보기

젼이·2024년 3월 7일

[Item 28].유효한 상태만 표현하는 타입 지정하기

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

예시 - 가정

  • State 타입에는 a와 b 속성은 반드시 가지고 c와 d 속성은 선택적인 속성이다.
  • 그리고 State 타입은 3가지 상태가 있다고 가정해보겠습니다. (One / Two / Three)
// 지양해야하는 방식 --> 속성 중심
interface StateProps {
   a:string; 
   b:string;
   c?:string;
   d?:string;
}

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;

[Item 29].사용할 때는 너그럽게, 생성할 때는 엄격하게

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

[Item 30].사용할 때는 너그럽게, 생성할 때는 엄격하게

  • 주석과 변수명에 타입 정보를 적는 것은 피해야한다. 최악의 경우에는 타입 정보에 모순이 발생한다.
  • 특정 매개 변수를 설명하고 싶다면 JSDoc의 @param 구문을 사용하자.

[Item 31]. 타입 주변에 null 값 배치하기

  • 오류를 걸러 내는 if 구문을 코드 전체에 추가해야 한다고 생각할 수 있다.
  • null과 null이 아닌 값을 섞어서 사용하면 클래스에서도 문제가 생긴다.
// 사용자와 그 사용자의 포럼 게시글을 나타내는 클래스를 가정해보자.
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 fetchPostsUser(userId)
 ]);
}
 getUserName() {
  ..//? 
 }
}

두번의 네트워크 요청이 로드되는 동안 user와 posts 속성은 null 상태이다.
어떤 시점에는 둘 다 null이거나, 둘 중 하나만 null이거나, 둘 다 null이 아닐 것이다.

속성 값의 불확실성이 클래스의 모든 메서드에 나쁜 영향을 미친다.
결국 null 체크가 난무하고 버그를 양산하게 된다.

// 사용자와 그 사용자의 포럼 게시글을 나타내는 클래스를 가정해보자.
class UserPosts {
	user: UserInfo | null;
  	posts: Post[] | null;

constructor(user: UserInfo, posts: Post[]) {
 this.user = null;
 this.posts = null;
}
  
static async init(userId: string): Promise<UserPosts> {
 return Promise.all([
  	fetchUser(userId),
    fetchPostsForUser(userId)
 ]);
  return new UserPosts(user, posts);
}
 getUserName() {
  return this.user.name;
 }
}

이제 UserPosts 클래스는 완전히 null이 아니게 되었고, 메서드를 작성하기 쉬워졌다.

null인 경우가 필요한 속성은 프로미스로 바꾸면 안 된다. 코드가 매우 복잡해지며 모든 메서드가 비동기로 바뀌어야한다. 프로미스는 데이터를 로드하는 코드를 단순하게 만들어 주지만, 데이터를 사용하는 클래스에서는 반대로 코드가 복잡해지는 효과를 내기도 한다.

  • 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하면 안된다.
  • API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null이거나 null이 아니게 만들어야한다. 사람과 타입 체커 모두에게 명료한 코드가 될 것이다.
  • 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.

[Item 32]. 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

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

// 2번
interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}
interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}
interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

의미상 1번보다 2번의 정의가 맞을때, 이러한 패턴을 태그된 유니온(또는 구분된 유니온)이라고 말한다.

interface FillLayer {
  type: 'fill';
...
}
interface LineLayer {
  type: 'line';
...
}
interface PointLayer {
  type: 'paint';
...
}
type Layer = FillLayer | LineLayer | PointLayer;
  • Layer를 나타내는 type 속성을 추가하면 ‘태그’로서 작동하며 어떤 타입의 Layer가 쓰이는지 판단된다.
  • 또한 type을 판단하는 if문을 통해 범위를 좁히는데도 사용한다.
  • 동시에 값이 있거나 없을 경우에도 태그된 유니온 패턴이 잘 맞다.
interface Person {
  name: string;
// place와 date가 둘 다 동시에 있거나 동시에 없을 때, 하나의 객체로 모아서 설계
  birth?: {
    place: string;
    date: Date;
  }
}

타입 구조에 손대지 못할 때도 인터페이스 유니온을 사용해서 속성간의 관계를 정의해줄 수 있다.

interface Name {
  name: string;
}

interface PersonWithBirth extends Name {
  placeOfBirth: string;
  dateOfBirth: Date;
}

type Person = Name | PersonWithBirth;

[Item 33]. string 타입보다 더 구체적인 타입 사용하기

  • 문자열을 남발하여 선언된 코드를 피해야 함 / string 타입보단 구체화된 타입을 사용하는 것이 좋다.
  • 문자열 리터럴 타입의 유니온을 사용해 타입 체크를 엄격히 하도록 해야한다.
  • 객체의 속성 이름을 매개변수로 받을 때는 keyof T를 사용하는 것이 좋다.
type RecordingType = 'studio' | 'live';

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: RecordingType;
}
  • 명시적으로 정의함으로써 다른곳으로 값이 전달되어도 타입 정보가 유지된다.
  • 타입을 명시적으로 정의하고 해당 타입의 의미를 설명하는 주석을 붙여 넣을 수 있다.
  • keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능하다.
  • string은 any와 같이 넓은 범위를 허용하여 타입간의 관계를 감추므로, string의 부분 집합을 정의하여 보다 정확한 타입을 사용해야 한다.
profile
코드도 짜고, 근육도 짜고

0개의 댓글