타입 주변에 null값 배치하기

다음의 최대, 최솟값을 계산하는 extent 함수가 있다고 생각해봅시다.

// WARNING : 실제로는 타입 에러가 발생합니다!
function extent(nums: number[]) {
  let min, max; // undefined, undefined
  for (const num of nums) {
    if (min === undefined) { // 최초에 min이 undefined라면 min, max 모두에 첫번째 값을 할당합니다.
      min = num;
      max = num;
    } else {
      // min에 대해서만 undefined 체킹이 이루어졌습니다.
      min = Math.min(min, num); // type min = number
      max = Math.max(max, num); // type max = number | undefined
    }
  }
  
  // 로직 만을 생각해본다면 반환 타입은 [number, number] | [undefined, undefined] 여야 하지만, 
  return [min, max];  // 실제로는 (number | undefined)[] 입니다.
}

위 함수는 로직 상 min, max는 둘 다 undefined 이거나, 둘 다 undefined가 아니어야 하지만, 이를 타입 체커가 인지하지 못 한다는 문제점을 갖고 있습니다. 그래서 실제로 strictNullChecks 환경에서 에러가 발생합니다.

함수의 반환 타입을 null이거나, null이 아니게 만드세요

이걸 해결하기 위해서는 해당 값들을 하나의 객체 또는 배열에 넣어 처리하면 됩니다. 반환값이 nullish하다면 반환 타입을 하나의 객체로 만들고 반환 타입 전체가 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; // [number, number] | null
}

const [min, max] = extent([0, 1, 2])!;

클래스 프로퍼티에는 null이 존재하지 않게 하세요

다음과 같이 프로퍼티에 nullish한 값이 존재한다면, 인스턴스를 생성하고 난 이후와 해당 클래스의 모든 메서드를 사용하기 어렵게 만듭니다.

class UserPosts {
  user: UserInfo | null;
  posts: Post[] | null;

  constructor() {
    // 최초 인스턴스 생성 시에 각 프로퍼티가 null 입니다.
    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() {
    // 각 메서드에서 매번 프로퍼티에 대한 null 체킹을 해주어야 합니다.
    if (this.user) return this.user.name;
    else return null;
  }
}

const userPost = new UserPosts();

// 인스턴스 생성 이후에도 계속 null 체킹이 필요합니다.
userPost.user // type UserInfo | 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() {
    // 이제 null 체킹이 필요 없습니다!
    return this.user.name;
  }
}