반응형
250x250
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

"모두 null" || "모두 null이 아닌 데이터"로 모델링 하기 본문

TS/이펙티브 타입스크립트

"모두 null" || "모두 null이 아닌 데이터"로 모델링 하기

5_hyun 2023. 3. 27. 17:27

타입만으로 어떤 변수가 null이 될 수 있는지 없는지를 표현하기는 어렵다. B에서 A의 값이 나오는 거면 A가 null이 될 수 없을 때 B도

null이 될 수 없다. 반대로 A가 null이 될 수 있으면 B도 null이 될 수 있다. 값이 전부 null이거나 전부 null이 아닌 경우로 구분해야 모델링하기 쉽다.

최댓값, 최솟값 예시

잘못된 코드

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)
      // ~~~ Argument of type 'number | undefined' is not
      //     assignable to parameter of type 'number'
    }
  }
  return [min, max]
}

이 코드는 max가 undefinded인 경우를 고려하지 못해서 에러가 발생했다. 에러를 해결할 수 있는 방법은 min과 max를 한 객체 안에 넣고 null이거나 null이 아니도록 모델링하면 된다.

개선한 코드

-"!" null 아님 단언문을 사용한 경우

function 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;
}
const [min, max] = extent([0, 1, 2])!;
const span = max - min; // OK

반환 타입이 [number, number]이거나 null이 되어 사용하기 쉬워졌다. "!" 단언문을 사용하면 min과 max를 얻을 수 있다.

 

-if 문을 사용한 경우

function 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;
}
const range = extent([0, 1, 2]);
if (range) {
  const [min, max] = range;
  const span = max - min; // OK
}

if문을 사용해서도 제대로 동작하게 구현할 수 있다.

클래스 사용 예제

잘못된 코드

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 fetchPostsForUser(userId)),
    ])
  }

  getUserName() {
    // ...?
  }
}

fetchUser와 fetchPostForUser는 서버로부터 데이터를 가져오는 요청 함수이다. 위 코드에서 두 요청이 실행되는 동안 user, post는 null이다. 어떤 시점에서는 둘 중 하나만 null일 수 있다. 2 * 2 하여 총 4가지의 경우의 수가 존재한다. 이러면 null 체크가 너무 빈번하게 되어 에러가 날 수 있다.

개선한 코드

class UserPosts {
  user: UserInfo
  posts: Post[]

  constructor(user: UserInfo, posts: Post[]) {
    this.user = user
    this.posts = posts
  }

  static async init(userId: string): Promise<UserPosts> {
    const [user, posts] = await Promise.all([fetchUser(userId), fetchPostsForUser(userId)])
    return new UserPosts(user, posts)
  }

  getUserName() {
    return this.user.name
  }
}

필요한 데이터가 모두 준비된 후에 클래스를 만들면 UserPosts가 완전히 null이 아니다. 클래스는 필요한 모든 값이 준비되면 생성하고 null이 존재하지 않는 게 좋다. 만약 null인 경우가 필요한 속성은 프로미스로 바꾸면 안 된다. 모든 메서드는 비동기로 바꿔줘야 한다. 

 

 

728x90
반응형

'TS > 이펙티브 타입스크립트' 카테고리의 다른 글

string보다 구체적 타입 사용하기  (0) 2023.03.29
인터페이스의 유니온 사용하기  (1) 2023.03.27
타입 설계의 중요성  (0) 2023.03.22
타입 추론과 문맥  (0) 2023.03.13
비동기는 async 사용  (0) 2023.03.09
Comments