스터디/이펙티브 타입스크립트

[이펙티브 타입스크립트] 3장 타입 추론 19~23

paran21 2024. 5. 26. 01:46

타입스크립트는 적극적으로 타입을 추론한다.

타입 추론을 잘 활용하면 타입을 명시적으로 선언하는 일이 줄어들기 때문에, 전체적인 안정성이 향상된다.

이번 장에서는 타입스크립트에서 타입 추론이 어떻게 이루어지는지, 주의해야하는 상황과 해결 방법에 대해 다룬다.

 

아이템 19. 추론 가능한 타입을 사용해 장황한 코드 방지하기

✔️ 타입 추론이 가능하다면 명시적으로 타입을 선언할 필요가 없다.

타입스크립트가 스스로 타입을 추론하지 못한다면 타입을 명시해야 한다.

타입스크립트에서 타입은 일반적으로 변수가 처음 등장할 때 결정된다.

 

장점 1. 리펙토링에 용이하다.

명시적으로 타입을 선언한 경우에는 타입이 변입이 변경되면 해당 변수를 사용하는 모든 곳에서 타입을 바꿔야 하지만, 타입 추론을 활용하면 변경할 필요가 없다.

interface User {
    //id의 타입이 string으로 변경된다면 타입이 선언된 부분만 수정하면 된다.
    id: number;
    name: string;
}

function logUser(user: User) {
    //타입 추론으로 타입을 명시하지 않았기 때문에 변경사항이 없다.
    const id = user.id;
    const name = user.name;
    console.log(id, name);
}

 

구조 분해 할당을 사용하면 모든 지역 변수의 타입이 추론되기 때문에 권장하는 방식이다.

function logUser(user: User) {
    const {id, name} = user;
    console.log(id, name);
}

 

장점2. 타입 구문을 생략하면 불필요한 것들을 생략하여 구현 로직에만 집중할 수 있게 한다.

 

✔️ 타입이 추론되어도 타입을 더 명시하는 게 나은 상황도 있다.

객체 리터럴을 정의할 때 타입을 명시하면 객체를 선언한 곳에서 에러를 확인할 수 있다.

일반적으로 이런 에러의 경우 선언된 객체의 타입에 오류가 있는 것이기 때문에 선언한 곳에서 에러가 표시되는 것이 낫다.

//id: number
//타입을 선언하지 않으면 객체를 선언한 곳이 아니라
const user = {
    id: '1234',
    name: 'userA'
}

//객체를 사용한 곳에서 에러가 발생한다.
logUser(user);

 

 

함수의 반환 타입을 명시하면 함수를 호출한 곳이 아닌 함수를 선언한 곳에서 에러를 확인할 수 있다.

//반환 타입과 함수 구현 내용이 달라서 에러 발생
function sayHello(user: User): string {
    const { name } = user;
    console.log(`Hello ${name}`);
}

 

반환 타입을 명시하면 함수를 더욱 명확하게 알 수 있고, interface나 type으로 명명된 타입을 사용할 수 있다.

 

아이템 20 다른 타입에는 다른 변수 사용하기

변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않는다.

다른 타입이라면 다른 변수를 사용하는 것이 낫다.

const id = '1234';
const numberId = 1234;

//타입을 명시하지 않으면 any로 추론된다.
let unionId: string | number;
unionId = '1234';
unionId = 1234;
  • 서로 관련이 없는 두 개의 값을 분리할 수 있다.
  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키고, 타입 구문이 불필요해진다.
  • 타입이 간결해진다.
  • const로 변수를 선언할 수 있다.

가려지는 변수는 혼란을 줄 수 있기 때문에 린터 규칙을 통해 사용하지 못하게 하는 것이 좋다.

const id = '1234';
{
    // 가려지는 (shadowed) 변수를 사용하면 동일한 변수명으로 혼란을 줄 수 있다.
    const id = 12345;
}

 

 

아이템 21 타입 넓히기

타입을 명시하지 않으면 타입 체커가 타입을 분석할 때 값을 가지고 할당 가능한 값들의 집합(즉, 타입)을 유추한다(넓히기 widening).

주어진 값으로 추론 가능한 타입이 여러 개이기 때문에 타입스크립트가 추측한 답이 의도와 다를 수 있다.

 

let 대신 const로 변수를 선언하면 더 좁은 타입이 된다.

객체와 배열은 const로 선언해도 추론할 수 있는 타입의 범위가 넓다. 객체의 경우 속성을 추가하는 것이 아니라 한 번에 만드는 것이 좋다(아이템 23)

 

✔️  타입 스크립트의 기본 동작을 재정의해 타입 추론의 강도를 제어할 수 있다.

  • 타입을 명시적으로 선언한다.
const v: {x: 1|3|5} = {
    x: 1,
}

 

  • 타입 체커에 추가적인 문맥을 제공한다(아이템 26)
  • const 단언문을 사용한다.
// { x: 1; y: number; }
const v2 = {
    x: 1 as const,
    y: 2
}

 

 

아이템 22 타입 좁히기

넓은 타입으로 선언된 변수의 타입은 더 좁은 타입으로 추론하는 과정을 말한다.

 

✔️ 조건문

function processValue(value: number | string) {
  if (typeof value === "string") {
    return value.toUpperCase(); // value는 string으로 좁혀짐
  } else {
    return value.toFixed(2); // value는 number로 좁혀짐
  }
}

// early return
function getLength(value: string | null) {
  if (value === null) {
    return 0; // early return으로 null 타입 제거
  }
  return value.length; // value는 string으로 좁혀짐
}

// throw error
function ensureNumber(value: number | undefined): number {
  if (value === undefined) {
    throw new Error("Value must be a number");
  }
  return value; // value는 number로 좁혀짐
}

 

 

✔️ instanceof

class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

function greet(object: User | Date) {
  if (object instanceof User) {
    console.log(`Hello, ${object.name}!`); // object는 User로 좁혀짐
  } else {
    console.log(`Today is ${object.toDateString()}.`); // object는 Date로 좁혀짐
  }
}

 

✔️ 속성 체크

interface Bird {
  fly(): void;
}

interface Fish {
  swim(): void;
}

function move(animal: Bird | Fish) {
  if ("fly" in animal) {
    animal.fly(); // animal은 Bird로 좁혀짐
  } else {
    animal.swim(); // animal은 Fish로 좁혀짐
  }
}

 

✔️ Array.isArray

function printFirstElement(elements: number[] | string) {
  if (Array.isArray(elements)) {
    console.log(elements[0]); // elements는 number[]로 좁혀짐
  } else {
    console.log(elements.charAt(0)); // elements는 string으로 좁혀짐
  }
}

 

 

✔️  태그된 유니온

type Circle = {
  kind: "circle";
  radius: number;
};

type Square = {
  kind: "square";
  sideLength: number;
};

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // shape는 Circle로 좁혀짐
    case "square":
      return shape.sideLength ** 2; // shape는 Square로 좁혀짐
  }
}

 

✔️  사용자 정의 타입 가드

function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}

// 타입은 string[]
const members = ['Janet', 'Michael'].map(
  who => jackson5.find(n => n === who)
).filter(isDefined);

 

아이템 23 한꺼번에 객체 생성하기

객체를 생성할 때 속성을 추가하는 것보다 한번에 선언하는 것이 타입 추론에 유리하다.

 

여러 객체를 조합해서 다른 객체를 만드는 경우에는 스프레드 연산자를 사용할 수 있다.

 

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5 };
const combinedObj = { ...obj1, ...obj2, ...obj3 };

 

특정 조건에 따라 객체에 속성을 추가하는 조건부 속성을 사용하고 싶다면 null 또는 {}를 사용하면 된다.

const baseObj = { name: "CursorBot", type: "AI" };

//조건부 속성
const featureObj = shouldAddFeature ? { feature: "Programming" } : {};

//타입은 { name: string; type: string; feature?: string; }
const combinedObj = { ...baseObj, ...featureObj };

 

선택적 필드를 추가하고 싶다면 헬퍼 함수를 사용할 수 있다.

// Partial을 사용해서 b의 속성들이 선택적 필드라는 것을 명시한다.
function addOptional<T extends object, U extends object>(
  a: T, b: U | null
): T & Partial<U> {
  return {...a, ...b};
}