icon안동민 개발노트

타입 가드와 타입 좁히기


 타입 가드는 타입스크립트에서 변수의 타입을 좁히는(narrowing) 방법을 제공하는 강력한 기능입니다.

 이를 통해 코드의 타입 안정성을 높이고, 런타임 오류를 줄일 수 있습니다.

타입 가드의 개념과 필요성

 타입 가드는 특정 스코프 내에서 변수의 타입을 보다 구체적으로 확정짓는 조건문입니다.

 이는 유니온 타입이나 'any' 타입과 같이 여러 가능한 타입을 가진 변수를 다룰 때 특히 유용합니다.

function processValue(value: string | number) {
    if (typeof value === "string") {
        // 이 블록 내에서 value는 string 타입으로 취급됨
        console.log(value.toUpperCase());
    } else {
        // 이 블록 내에서 value는 number 타입으로 취급됨
        console.log(value.toFixed(2));
    }
}

기본적인 타입 가드 구현

  1. typeof 연산자
function printLength(value: string | number) {
    if (typeof value === "string") {
        console.log(value.length); // string의 length 속성 사용 가능
    } else {
        console.log(value.toFixed(2)); // number의 toFixed 메서드 사용 가능
    }
}
  1. instanceof 연산자
class Dog {
    bark() { console.log("Woof!"); }
}
 
class Cat {
    meow() { console.log("Meow!"); }
}
 
function makeSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        animal.bark();
    } else {
        animal.meow();
    }
}
  1. in 연산자
interface Bird {
    fly(): void;
}
 
interface Fish {
    swim(): void;
}
 
function move(pet: Bird | Fish) {
    if ("fly" in pet) {
        pet.fly();
    } else {
        pet.swim();
    }
}

사용자 정의 타입 가드

 복잡한 타입 검사를 위해 사용자 정의 타입 가드 함수를 만들 수 있습니다.

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type Shape = Rectangle | Circle;
 
function isRectangle(shape: Shape): shape is Rectangle {
    return shape.kind === "rectangle";
}
 
function calculateArea(shape: Shape) {
    if (isRectangle(shape)) {
        return shape.width * shape.height;
    } else {
        return Math.PI * shape.radius ** 2;
    }
}

 여기서 isRectangle 함수는 shape is Rectangle을 반환 타입으로 사용하여 사용자 정의 타입 가드를 정의합니다.

식별 가능한 유니온 타입

 식별 가능한 유니온 타입은 공통 속성을 사용하여 타입을 구분하는 패턴입니다.

interface Square {
    kind: "square";
    size: number;
}
 
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
 
type Shape = Square | Rectangle;
 
function calculateArea(shape: Shape) {
    switch (shape.kind) {
        case "square":
            return shape.size ** 2;
        case "rectangle":
            return shape.width * shape.height;
    }
}

 이 패턴은 타입 안정성을 높이고 코드의 가독성을 개선합니다.

never 타입을 활용한 철저한 검사

 never 타입을 사용하여 모든 가능한 케이스를 처리했는지 확인할 수 있습니다.

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
 
function calculateArea(shape: Shape) {
    switch (shape.kind) {
        case "square":
            return shape.size ** 2;
        case "rectangle":
            return shape.width * shape.height;
        default:
            return assertNever(shape); // 새로운 shape 타입이 추가되면 컴파일 오류 발생
    }
}

 이 접근 방식은 새로운 타입이 추가될 때 처리되지 않은 케이스를 쉽게 찾을 수 있게 해줍니다.

조건부 타입 처리

 타입 가드를 사용한 조건부 타입 처리는 코드의 타입 안정성을 크게 개선할 수 있습니다.

type StringOrNumber = string | number;
 
function double(value: StringOrNumber): StringOrNumber {
    if (typeof value === "string") {
        return value.repeat(2);
    } else {
        return value * 2;
    }
}

타입 가드 vs 타입 단언

 타입 가드는 런타임에 동작하는 검사를 수행하지만, 타입 단언은 컴파일러에게 타입 정보를 강제로 알려주는 방식입니다.

// 타입 가드
if (typeof value === "string") {
    console.log(value.toUpperCase());
}
 
// 타입 단언
console.log((value as string).toUpperCase());

 타입 가드는 런타임 안전성을 제공하므로 가능한 한 타입 단언보다 타입 가드를 사용하는 것이 좋습니다.

제네릭과 타입 가드

 제네릭과 타입 가드를 함께 사용하여 재사용 가능한 타입 안전 코드를 작성할 수 있습니다.

function isOfType<T>(value: any, propertyToCheck: keyof T): value is T {
    return (value as T)[propertyToCheck] !== undefined;
}
 
interface User {
    name: string;
    email: string;
}
 
function processUser(obj: any) {
    if (isOfType<User>(obj, 'email')) {
        console.log(obj.email);
    }
}

 이 패턴을 사용하면 다양한 타입에 대해 재사용 가능한 타입 가드를 만들 수 있습니다.

주의사항과 전략

  1. 부작용 방지 : 타입 가드 함수는 순수 함수여야 하며, 부작용이 없어야 합니다.
  2. 성능 고려 : 복잡한 타입 가드는 런타임 성능에 영향을 줄 수 있으므로, 필요한 경우에만 사용해야 합니다.
  3. 가독성 유지 : 너무 복잡한 타입 가드는 코드의 가독성을 해칠 수 있으므로, 적절히 분리하고 명확한 이름을 사용해야 합니다.

Best Practices

  1. 가능한 한 구체적인 타입을 사용하여 타입 가드의 필요성을 줄입니다.
  2. 식별 가능한 유니온 타입을 활용하여 명확한 타입 구분을 제공합니다.
  3. 사용자 정의 타입 가드 함수를 만들어 복잡한 타입 검사를 캡슐화합니다.
  4. never 타입을 활용하여 철저한 타입 검사를 구현합니다.
  5. 타입 가드와 제네릭을 조합하여 재사용 가능한 타입 안전 코드를 작성합니다.
  6. 타입 단언보다는 타입 가드를 우선적으로 사용합니다.
  7. 복잡한 타입 가드 로직은 별도의 함수로 분리하여 가독성을 높입니다.
  8. 타입 가드 함수의 이름은 의도를 명확히 표현해야 합니다 (예 : isUser, hasEmail 등).
  9. 타입 가드를 테스트 코드로 검증하여 정확성을 보장합니다.
  10. 타입 시스템의 한계를 인지하고, 필요한 경우 런타임 검사를 병행합니다.

 타입 가드와 타입 좁히기는 타입스크립트에서 코드의 타입 안정성을 크게 향상시키는 강력한 도구입니다.

 타입 가드는 단순히 타입 오류를 방지하는 것을 넘어, 코드의 논리적 흐름을 명확히 하고 자기 문서화 코드를 만드는 데도 도움이 됩니다.