본문으로 건너뛰기
김신건의 로그

[TypeScript] Type Narrowing (Type Guards)

· 수정 · 📖 약 2분 · 706자/단어 #typescript #types #narrowing #control-flow
TypeScript Narrowing, TypeScript Type Guards, TS narrowing, type predicate, user-defined type guard, asserts type guard, assertion function, TypeScript 타입 가드

정의

Type Narrowing 은 TypeScript 컴파일러가 제어 흐름 분석 (control flow analysis) 을 통해 특정 위치에서 값의 타입을 더 좁게 추론하는 기능입니다. typeof, instanceof, in, 리터럴 비교, 사용자 정의 type guard 로 union 을 각 branch 별 구체 타입으로 좁힙니다.

typeof narrowing

primitive 타입 (string, number, boolean, bigint, symbol, undefined, object, function) 검사.

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;   // padding: number
  }
  return padding + input;                  // padding: string
}

instanceof narrowing

class Cat { meow() {} }
class Dog { bark() {} }

function pet(x: Cat | Dog): void {
  if (x instanceof Cat) {
    x.meow();     // x: Cat
  } else {
    x.bark();     // x: Dog
  }
}

함정: interface 는 instanceof 못 씀 (런타임에 없음). class 여야 함.

in operator

객체에 특정 필드가 있는지 검사.

type Cat = {meow(): void};
type Dog = {bark(): void};

function pet(x: Cat | Dog): void {
  if ("meow" in x) {
    x.meow();     // x: Cat
  } else {
    x.bark();     // x: Dog
  }
}

Interface (런타임 정보 없음) 에도 적용 가능.

Truthiness narrowing

function getLength(s: string | null | undefined): number {
  if (s) {
    return s.length;    // s: string (null, undefined, "" 배제)
  }
  return 0;
}

주의: if (s) 는 falsy 모두 배제. "", 0, false, NaN, null, undefined. 명시적으로 원하는 것만 배제:

if (s !== null && s !== undefined) { ... }
if (s != null) { ... }              // == null 은 null + undefined

Equality narrowing

리터럴 비교로 좁힘:

function example(x: string | number, y: string | boolean): void {
  if (x === y) {
    // 둘 다 string 이어야 == 가능
    x.toUpperCase();   // x: string
    y.toUpperCase();   // y: string
  }
}

Discriminated Union

Union 의 각 case 에 리터럴 필드 (discriminant) 를 두어 좁힘.

type Shape =
  | {kind: "circle"; radius: number}
  | {kind: "square"; side: number};

function area(s: Shape): number {
  if (s.kind === "circle") {
    return Math.PI * s.radius ** 2;   // s: {kind: "circle", radius: number}
  }
  return s.side ** 2;                  // s: {kind: "square", side: number}
}

자세한 것은 Union / Intersection 참조.

Assignment narrowing

let x: string | number;
x = "hello";
x.toUpperCase();     // x: string
x = 42;
x.toFixed(2);         // x: number

Assignment 이후 실제 타입으로 좁혀짐. 이 좁힘은 다음 재할당까지 유효.

Control flow analysis

TypeScript 는 return, throw, continue 등을 인식.

function process(x: string | null): string {
  if (x === null) {
    throw new Error("null!");
  }
  return x.toUpperCase();    // x: string (null 은 throw 로 배제됨)
}

User-Defined Type Guard (is)

함수의 return type 을 arg is Type 으로 선언하면 컴파일러가 그 함수를 type guard 로 인식.

interface Cat { meow(): void }
interface Dog { bark(): void }

function isCat(x: Cat | Dog): x is Cat {
  return "meow" in x;
}

function pet(x: Cat | Dog): void {
  if (isCat(x)) {
    x.meow();     // x: Cat
  } else {
    x.bark();     // x: Dog
  }
}

실전 예시

function isNonNull<T>(v: T | null | undefined): v is T {
  return v != null;
}

const users = [user1, null, user3, undefined, user5];
const validUsers = users.filter(isNonNull);
// validUsers: User[] (User | null | undefined 에서 좁혀짐)

배열 filter narrowing (TS 5.5+)

TypeScript 5.5 부터는 filter 안 predicate 를 자동으로 type guard 로 인식:

const validUsers = users.filter((u): u is User => u != null);
// 또는 5.5+ 에서는 자동
const validUsers = users.filter(u => u != null);

Assertion Function (asserts)

함수가 조건 검증에 실패하면 예외 던짐. 이후 코드는 조건이 참이라고 가정.

function assertNumber(x: unknown): asserts x is number {
  if (typeof x !== "number") {
    throw new Error("not a number");
  }
}

function process(x: unknown): void {
  assertNumber(x);
  x.toFixed(2);    // x: number (이 시점부터 좁혀짐)
}

assertNever (exhaustive check)

function assertNever(x: never): never {
  throw new Error(`unexpected: ${x}`);
}

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "square": return s.side ** 2;
    default: return assertNever(s);
  }
}

새 case 를 추가하면 컴파일 시 오류. Discriminated union 의 필수 도구.

비타 - Node.js assert 모듈과 통합

import assert from "node:assert";

function process(x: unknown): void {
  assert(typeof x === "string");
  x.toUpperCase();    // x: string
}

node:assertasserts 를 반환하므로 자동 narrowing 대상.

Array narrowing

function last<T>(arr: T[]): T | undefined {
  if (arr.length > 0) {
    return arr[arr.length - 1];   // 여전히 T | undefined
  }
  return undefined;
}

arr.length > 0 은 배열의 element 타입에 영향 없음 (T[] 여도 arr[i]T | undefined 로 좁혀지지 않음. 그럼에도 접근 가능한 이유는 strictNullChecks 만으로는 noUncheckedIndexedAccess 없이 배열 접근이 T 로 낙관 취급됨).

noUncheckedIndexedAccess: true 활성 시 arr[i]T | undefined.

in-operator narrowing 심화

TS 4.9+: in 이 optional 필드도 좁힘.

type Cat = {meow(): void; name?: string};
type Dog = {bark(): void; name?: string};

function example(x: Cat | Dog): void {
  if ("meow" in x) {
    x.meow();
    // x.name?: string  (optional 필드)
  }
}

Class field narrowing

class Animal {
  static isCat(x: Animal): x is Cat {
    return x instanceof Cat;
  }
}

Static method 로 type guard.

함정

WARNING

typeof null === "object". null 검사는 별도. if (typeof x === "object" && x !== null).

CAUTION

instanceof 는 class 만. Interface, type alias 로 정의된 것에는 못 씀.

WARNING

User-defined type guard 는 신뢰의 문제. function isCat(x): x is Cat { return true } 도 컴파일 통과. 실제 검증을 정확히 해야.

IMPORTANT

asserts 는 반드시 예외 던지거나 return 안 함. 반환하면 assertion 이 아님. never 반환 타입도 고려.

CAUTION

Truthiness narrowing 의 부작용. if (x) 는 falsy 모두 배제. "", 0 이 유효값이면 명시적 != null 사용.

WARNING

in narrowing 은 상속 필드 못 검사. Prototype chain 상 없어도 in 은 true. Own property 만 체크는 Object.hasOwn.

관련 위키

이 글의 용어 (9개)
[Javascript] boolean / null / undefinedjavascript
정의 JavaScript 의 3 가지 primitive. - : , - : 의도적인 "없음" - : 자동으로 부여된 "없음" (초기화 안 됨) 사용 상황 | 상황 | 권장 값 /…
[TypeScript] Conditional Typestypescript
정의 Conditional Type 은 형태로 타입 레벨에서 조건 분기를 표현합니다. 키워드로 매칭된 타입을 추출하고, union 에 대해서는 자동 분배 (distributive…
[TypeScript] Genericstypescript
정의 Generics 는 타입을 파라미터화하는 문법입니다. 함수, 클래스, 인터페이스, type alias 가 재사용 가능 하면서도 타입 안전 하도록 만듭니다. Java 의 ge…
[TypeScript] Interfacestypescript
정의 Interface 는 객체의 shape (형태) 을 정의하는 TypeScript 문법입니다. 필드, 메서드, 인덱스 시그니처, 호출 시그니처를 선언하며, 로 상속하고 dec…
[TypeScript] Primitive Typestypescript
정의 Primitive Types 는 TypeScript 의 가장 기본 타입 계열입니다. JavaScript 의 7가지 primitive 값 + 특수 타입 (void, never…
[TypeScript] Strict Mode & tsconfigtypescript
정의 Strict Mode 는 여러 엄격한 타입 검사 플래그를 한 번에 켜는 옵션 ( ) 입니다. 강력한 타입 안전성을 제공하지만, 기존 코드베이스에는 대량의 오류를 발생시킬 수…
[TypeScript] Union & Intersection Typestypescript
정의 - Union Type ( ): A 이거나 B (또는 둘 다) 인 값 - Intersection Type ( ): A 이면서 B 인 값 두 조합은 TypeScript 의 타…
[TypeScript] Utility Typestypescript
정의 Utility Types 는 TypeScript 표준 라이브러리에 포함된 미리 정의된 generic 타입 들입니다. 기존 타입을 변형/구성하는 흔한 패턴을 제공하며, map…
TypeScripttypescript
정의 TypeScript 는 Microsoft 가 2012년 발표한 JavaScript 의 상위 집합 프로그래밍 언어입니다. 정적 타입 시스템, 인터페이스, 제네릭, enum, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기