Narrowing

코드의 안정성과 신뢰성을 높여주는 타입을 좁히는 여섯가지 방법을 소개합니다.

타입스크립트에서 Narrowing은 타입의 범위를 좁히는 것을 의미합니다. 이는 변수나 표현식의 타입을 보다 구체적으로 추론하거나 지정하여 코드의 안전성을 높이는 과정을 말합니다.

Type Guard

이번 섹션에서는 타입을 좁히는 여섯 가지 방법을 소개합니다. 이 방법들을 통해 코드의 타입을 명확하게 정의하고, 잠재적인 오류를 사전에 방지할 수 있습니다. 이를 통해 타입스크립트의 안전성과 코드의 신뢰성을 더욱 높일 수 있습니다.

➡️ 조건문을 이용한 타입 확인

➡️ typeof 타입 가드

➡️ Instanceof 타입 가드

➡️ in 타입 가드

➡️ type predicate 를 이용한 타입 가드

➡️ assertion signature 을 이용한 타입 가드

1. 조건문을 이용한 타입 확인

type Animal = {
  name: string;
  legs?: number;
};

Animal 타입을 이용하는 아래 addLeg 함수는 타입 에러가 발생할 수 있습니다.

function addLeg(animal: Animal) {
  animal.legs = animal.legs + 1; // 💥 - Object is possibly 'undefined'
}

legs 프로퍼티가 undefined가 될수 있기 때문에 타입 에러가 발생할 수 있습니다. 그리고 undefined1을 더하는 것 또한 말이 되지 않습니다. 지금 상황에서 타입 에러를 피하기 위해서는 legs 프로퍼티를 이용하기 전에 legstruthy인지 확인하면 됩니다.

function addLeg(animal: Animal) {
  if (animal.legs) { 
    animal.legs = animal.legs + 1;
  }
}

legsif문 전에는 number | undefined 타입입니다. 하지만, if문후에는 number 타입으로 좁혀지게(narrow) 됩니다.

이 방식의 Type narrowing은 여러 타입을 가질 수 있는 변수에서 nullundefined를 제외시키는데에 유용한 방법입니다.

2. typeof 타입 가드

인자가 string이면 복사하여 붙이고, number라면 곱하기 2를 하는 아래 함수를 살펴봅시다.

function double(item: string | number) {
  if (typeof item === "string") {
    return item.concat(item); // item is of type string
  } else {
    return item + item; // item is of type number
  }
}

item 인자는 if문전에는number혹은 string 타입입니다. if문안에서는 string으로 좁혀지고, else문에서는 number로 좁혀지게 됩니다.

위 방식을 typeof 타입 가드 패턴이라고 합니다. 원시 타입을 type narrowing할 때 유용합니다.

3. Instanceof 타입 가드

우리가 사용하는 타입들은 보통 원시 타입보다 복잡합니다. 아래 코드를 살펴보죠.

class Person {
  constructor(
    public firstName: string,
    public surname: string
  ) {}
}
class Organisation {
  constructor(public name: string) {}
}
type Contact = Person | Organisation;

아래 함수는 타입에러가 발생합니다.

function sayHello(contact: Contact) {
  console.log("Hello " + contact.firstName);
  // 💥 - Property 'firstName' does not exist on type 'Contact'.
}

에러의 이유는 contactfirstName프로퍼티를 가지지않은 Organisation 타입일 수 있기 때문입니다. 클래스 타입을 이용할 때 instanceof 타입 가드를 이용하면 편합니다.

function sayHello(contact: Contact) {
  if (contact instanceof Person) {    
    console.log("Hello " + contact.firstName);
  }
}

if문안에서 contact의 타입은 Person으로 좁혀집니다(narrow). 그러므로 위에서 발생했던 에러를 피할 수 있게 됩니다.

4. in 타입 가드

우리는 타입을 표현할 때 항상 class를 이용하진 않습니다. 위에서 클래스를 이용하여 표현된 타입을 아래처럼 바꿀 수 있습니다(with interface)

interface Person {
  firstName: string;
  surname: string;
}
interface Organisation {
  name: string;
}
type Contact = Person | Organisation;

instanceof 타입 가드는 interface 혹은 타입 별명(aliases)들에는 작동하지 않습니다. 그대신에 우리는 in 타입 가드를 이용할 수 있습니다.

function sayHello(contact: Contact) {
  if ("firstName" in contact) {    
    console.log("Hello " + contact.firstName);
  }
}

if문안에서 contact는 Person 타입으로 좁혀지고 타입 에러가 발생하지 않게 됩니다.

5. type predicate 를 이용한 타입 가드

type Rating = 1 | 2 | 3 | 4 | 5;

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();
  const rating = product.rating;
  return rating;
}

위 함수의 리턴 타입은 Promise<any> 로 추측됩니다. 그래서 이 함수의 리턴값에 변수를 할당하면 any 타입을 가지게 됩니다.

const rating = await getRating("1"); // type of rating is `any`

이는 아무런 타입 확인이 일어나지 않는다는 말입니다.

type predicate를 가지게 함수를 만들면 우리의 코드는 더욱 타입에 안정적일 수 있습니다.

function isValidRating(
  rating: any
): rating is Rating {
  if (!rating || typeof rating !== "number") {
    return false;
  }
  return (
    rating === 1 ||
    rating === 2 ||
    rating === 3 ||
    rating === 4 ||
    rating === 5
  );
}

rating is Rating은 위 함수에서 타입 서술어type predicate입니다. type predicate가 사용된다면 반드시 boolean값을 리턴해야 합니다. 아래 코드는 위에서 정의한 type predicate를 가진 isValidRating 함수를 이용한 getRating 함수입니다.

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();  const rating = product.rating;
  if (isValidRating(rating)) {
    return rating; // type of rating is `Rating`
  } else {
    return undefined;
  }
}

isValidRating()을 이용하였기 때문에 if문안에서 rating의 타입은 Rating이 됩니다.

6. assertion signature 을 이용한 타입 가드

getRating을 조금 다르게 타입가드해보려고 합니다.

async function getRating(productId: string) {
  const response = await fetch(
    `/products/${productId}`
  );
  const product = await response.json();  const rating = product.rating;
  checkValidRating(rating); // should throw error if invalid rating
  return rating; // type should be narrowed to `Rating`
}

checkValidRating 함수를 추가했습니다. 이로 인해 getRating함수는 rating이 유효하지 않은 값일 때 에러를 발생하게 됩니다.

그리고 checkValidRating이 성공적으로 실행된다면 그 결과로 rating의 타입은 알아서 좁혀지게(narrowed) 됩니다.

assertion 시그니처와 함께 타입 가드 함수를 만들수 있습니다.

function checkValidRating(
  rating: any
): asserts rating is Rating {
  if (!rating || typeof rating !== "number") {
    throw new Error("Not a rating");
  }
  if (
    rating !== 1 &&
    rating !== 2 &&
    rating !== 3 &&
    rating !== 4 &&
    rating !== 5
  ) {
    throw new Error("Not a rating");
  }
}

asserts rating is Rating 은 위 함수의 assertion signature입니다. 만약 위 함수가 에러없이 리턴된다면 rating 인자의 타입은 Rating의 타입으로 범위가 좁혀집니다.

Reference

Last updated