본문 바로가기
Typescript

[TypeScript] 이펙티브 타입스크립트 정리 - 아이템 26 ~ 31

by 맨날개발 2025. 6. 28.
이펙티브 타입스크립트 읽고 정리
아이템 1 ~ 5 보러가기
아이템 6 ~ 8 보러가기
아이템 9 ~ 13 보러가기
아이템 14 ~ 16 보러가기
아이템 17 ~ 19 보러가기
아이템 20 ~ 25 보러가기

 

2️⃣6️⃣ 아이템 26. 타입 추론에 문맥이 어떻게 사용되는지 이해하기

함수에 값을 전달할 때 리터럴과, 변수에 값을 담아서 사용하는 경우 차이가 발생할 수 있다.

type AB = 'A' | 'B'

function fn(ab: AB) {}

fn('A');

let a = 'A';

fn(a); // 타입 에러 발생 

 

위의 코드를 보면 동일한 A를 전달하고 있지만 변수 a값을 전달하는 경우 타입에러가 발생한다. 그 이유는 타입이 추론되는 시점이 다르기 때문이다.

 

리터럴 A는 함수를 호출할 때 전달되며 이 시점에서는 변수 타입 AB에 값을 할당하는 것이기 때문에 'A' 타입으로 추론이 되기 때문에 정상동작한다.

 

하지만 let a = 'A' 에 할당되는 건 변수 let에 할당되는 것이기 때문에 string 타입으로 추론된다.

 

튜플과 객체의 사용시에도 비슷한 문제가 발생한다.

function fn1(tuple: [number, number]) {};

const list = [1,2];

fn1(list); // 타입 에러 발생

interface A {
  name: 'Hello' | 'Hi';
}

function fn2(a: A) {};

const a = {
  name: 'Hello'
};

fn2(a); // 타입 에러 발생

 

위의 코드를 실행했을 때 타입에러가 발생하는 이유는, 객체의 값은 변경이 가능하기 때문에 타입 넓히기가 실행되기 때문이다.

 

위와 같은 문제를 해결하기 위해서는 객체를 변수에 저장할 때 타입을 지정해주는 것이 좋다. 변수는 한가지 용도로만 사용하고 다른 용도로 사용한다면 새로운 변수를 생성하는 것이 좋다.

 

이렇게 사용한다면 객체에 타입을 명시적으로 작성해도 문제가 되지 않는다.

 

 

2️⃣7️⃣ 아이템 27. 함수형 기법과 라이브러리로 타입 흐름 유지하기

타입 흐름을 개선하고, 가독성을 높이고, 명시적 타입 구문의 필요성을 줄일때 로대시와 같은 유틸리티 라이브러리의 도움을 받을 수 있다.

 

 

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

하나의 상태에 표현 가능한 모든 경우를 포함하는 경우라면, 정확히 어떠한 동작을 해야하는지 코드만으로는 파악하기 힘들게 만든다.

 

아래의 코드는 현재 로딩중인지, 로딩 완료 후 에러인지 정상적인 결과인지에 따른 모든 상태에 따른 타입을 포함하고 있다.

interface State {
  isLoading: boolean;
  error?: string;
  result?: string;
}

 

위의 코드의 문제점은 현재의 상태만으로는 정확히 어떠한 결과를 사용해야하는지 파악하기 힘들다. 예를들어 result이면서 error가 존재할 수 있는 것인지 정확하게 알 수 없다.

 

이러한 경우 실제 유효한 상태로 분리하는 것이 좋다.

interface RequestPending {
  state: 'pending';
}

interface RequestError {
  state: 'error';
  error: string;
}

interface RequestSuccess {
  state: 'ok';
  result: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess

 

 

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

함수의 매개변수의 타입은 범위를 넓게 설정해도 되지만, 반환할때는 타입의 범위가 구체적이어야 한다.

 

보통 매개변수의 타입 범위를 넓히기 위해서 유니온 또는 선택적 속성을 활용한다. 일반적으로 넓어야 하긴 하지만 경우의 수가 너무나 넓은 것도 좋은 설계는 아니다. 라이브러리를 위한 용도로 사용하는 경우에는 넓은 범위를 지원해야하긴 하지만, 그런 경우가 아니라면 적당한 범위 내에서 넓혀서 사용해야 한다.

 

반환 타입에서 유니온 또는 선택적 속성을 사용하는 경우에느 함수 호출 후 추가 적인 조건문을 필요로하게 만든다. 그렇기 때문에 구체적인 타입으로 지정해주는 것이 좋다.

interface A {
  type: 'A';
}

interface B {
  type: 'B';
}

function fn(a: A | B): A | B {
  return a;
}

const a = fn({
  type: 'A'
});

if (a.type === 'A') {
  // 
}

 

위의 코드처럼 반환 타입이 구체적이지 않은 경우, 반환된 값을 조건을 통해서 타입을 좁혀줘야한다. 그래서 구체적인 타입을 반환하는 것이 좋다.

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

 

 

3️⃣0️⃣ 문서에 타입 정보를 쓰지 않기

타입스크립트는 자바스크립트에서 할 수 없던 함수의 입출력에 타입을 지정함으로써, 어떠한 값을 사용해야하는지를 명시할 수 있게 되었다.

 

자바스크립트만 사용하던 경우에는 함수의 사용방법을 주석으로 추가하고 입출력에 대한 정보를 주석에 포함했었다. 하지만 이제는 타입스크립트로만 작성하면 이 부분에 대한 정보를 코드 자체에서 포함하고 있으니 주석에는 따로 표시하지 않는 것이 좋다.

 

코드와 달리 주석은 IDE에서 따로 체크를 받을 수 없으니 누락되는 경우가 발생할 수 있다. 이는 주석과 코드가 불일치를 발생시킬 수 있으며, 사용할때 혼돈을 줄 수 있다.

✨ 타입스크립트를 사용할 때는 주석에는 함수에 대한 설명만 추가하는 것이 좋다.

 

만약 숫자타입을 사용하지만 단위가 필요하다면 주석보다는 변수명에 단위를 포함하는 것을 고려하는 것이 좋다.

function fn(time: number) {}

function fn(timeMs: number) {}

 

하지만 변수명에 타입을 포함하는 것은 좋지 못하다. 어차피 타입으로 유추할 수 있는 내용인데 중복 표기가 되는 꼴이기 때문이다.

const ageNum: number = 20; // X
const age: number = 20; // O

 

 

3️⃣1️⃣ 타입 주변에 null 값 배치하기

함수에서 여러값을 반환할때 값마다 개별로 선언후 사용하는 것보다는 하나의 그룹으로 사용하는 것이 좋다.

 

아래는 숫자 배열을 전달하면 최소값과 최대값을 반환하는 함수이다. 보통 min, max 변수를 따로 생성 후 반환 시에 배열형태로 반환해주는 코드를 작성하기 쉽다.

 

아래의 코드의 문제는 다음과 같다.

  • nums에 아이템이 존재하지 않는 경우 undefined를 반환하게 된다.
  • max에 대한 타입 체크를 하지 않았기 때문에 타입 에러가 발생한다.
function minMax(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];
}

 

해당 함수를 사용 후 반환값에서도 배열에 담긴 아이템의 undefined 체크를 해주어야 한다. 반환 타입이 (number | undefiend)[] 이기때문!

 

이제 min, max를 하나로 묶어서 표현하면 기존보다 사용하기 편하게 바뀐다.

function minMax(nums: number[]) {
  let result: [number, number] | null = null;

  for (const num of nums) {
    if (!result) {
      result = [num, num];
    } else {
        result = [
          Math.min(result[0], num),
          Math.max(result[1], num)
        ]
    }
  }

  return result;
}

 

변경된 코드에서의 반환값은 null 아니면 [number, number] 튜플이 되었다. 기존에는 배열의 아이템이 number | undefined 였기 때문에 개별 undefined 체크하는 단점이 있었다. 하지만 변경된 코드는 반환값이 null 인지 한번만 체크하면 된다.

 

클래스를 설계할 때 내부 속성의 타입이 객체인 경우 해당 속성이 null을 포함하면 해당 클래스의 메서드에 나쁜 영향을 주게 된다.

아래와 같이 null을 포함하게 되는 경우 두 속성을 사용하는 메서드 내에서는 null 조건을 체크하는 로직을 포함해야 한다.

class A {
  user: User | null;
  posts: Post[] | null;
}

 

보통 null 타입을 가지는 경우는, 객체가 null과 인스턴스로 계속해서 변경되는 경우는 드물다. 인스턴스 값의 준비가 되지 않아 null로 초기화 해두고 준비가 완료되면 인스턴스로 저장할 때 사용하는 경우가 많을 것이다.

 

만약 그런상황이라면 null 타입을 가지는 것보다는 준비가 모두 완료 된 경우에 객체를 생성하는 것이 좋다.

class A {
  user: User;
  posts: Post[];

  static async init(userId: number): Promise<A> {
    const [user, posts] = await Promise.all([
      fetchUSer(userId),
      fetchPosts(userId)
    ]);

    return new A(user, posts);
  }
}

 

✨ 준비가 덜 된 상태로 객체를 생성 후 사용해야 한다면 null을 허용해도 된다.

 

 

🙄 정리

  • 함수의 파리미터로 값을 전달할 때 리터럴 또는 변수에 따라 달리 타입 추론이 달라질 수 있으니 이에 유의하자.
  • 타입을 정의할 때 하나의 타입에 가능한 모든 속성을 추가하고 옵셔널으로 사용하지 말자. 각 상태에 따라 필요한 속성으로만 정의하자. 그러지 않으면 코드를 파악하기 힘들고 조건이 더 많이 들어가게 된다.
  • 함수의 매개변수 타입에는 어느정도 느슨하게, 반환할 때는 구체적인 타입으로 정의하자.
  • 클래스를 설계할 때 내부 속성의 타입이 객체인경우 특정 상황에 null이어야 하는 경우가 존재하는게 아니라면 null 타입으로 초기화하지 않도록 하자. 객체가 null 타입을 가지는 경우 메서드에서 해당 객체를 사용할 때마다 조건을 추가해야한다.