본문으로 건너뛰기

프론트엔드

TypeScript 타입 단언(as)과 타입 좁히기

2026. 06. 22. 월요일 오후 8시 30분

타입스크립트에서 컴파일러가 타입 에러를 표시할 때, as로 타입을 단언하면 에러가 사라집니다. 값의 타입을 개발자가 알고 있다고 생각할 때 흔히 쓰는 방법입니다.

하지만 as로 에러를 지워도 타입 오류가 없어지는 것은 아닙니다. 이 글에서는 as가 어떤 검사를 건너뛰는지, 그리고 검사를 건너뛰지 않고 타입을 좁히는 방법을 정리합니다.

as가 건너뛰는 컴파일러 검사

타입 단언(Type Assertion)은 개발자가 값의 타입을 직접 지정하는 문법입니다. 컴파일러는 단언한 타입을 그대로 받아들이고 더 검사하지 않습니다. 그래서 단언한 타입이 실제 값과 다르면 문제가 생깁니다.

// 서버 응답처럼, 타입을 알 수 없는 값이라고 가정한다.
const raw: unknown = JSON.parse('"42"'); // 실제 값은 문자열 "42"
 
// 값을 확인하지 않고 number로 단언한다.
const price = raw as number;
 
// 타입 검사는 통과한다. price는 number로 취급되기 때문이다.
console.log(price.toFixed(2));

raw의 실제 값은 문자열 "42"지만, as number로 단언했기 때문에 컴파일러는 price를 number로 봅니다. 타입 검사는 통과합니다. 하지만 문자열에는 toFixed가 없으므로 실행하면 에러가 납니다.

npx tsx 실행 결과로 TypeError: price.toFixed is not a function이 출력된 터미널 화면"npx tsx 실행 결과로 TypeError: price.toFixed is not a function이 출력된 터미널 화면"

타입 검사를 통과했는데 런타임에 TypeError: price.toFixed is not a function이 났습니다. 타입스크립트를 쓰는 이유가 이런 에러를 컴파일 타임에 잡으려는 것인데, as가 그 검사를 껐습니다.

as는 값을 변환하지 않습니다. 런타임에는 아무 일도 하지 않고, 컴파일러가 값을 다른 타입으로 보게 할 뿐입니다. 그래서 단언이 틀려도 막는 장치가 없습니다.

typeof, in, instanceof로 좁히기

as 대신 쓸 수 있는 방법은 타입 좁히기(Narrowing)입니다. 타입스크립트는 조건문 안에서 값의 타입을 자동으로 좁혀서, 분기마다 가능한 타입만 남깁니다.

먼저 좁히지 않은 경우입니다. string | number를 받아 바로 문자열 메서드를 호출하면 컴파일 에러가 납니다.

function format(value: string | number): string {
  return value.trim();
  // error TS2339: Property 'trim' does not exist on type 'string | number'.
  //   Property 'trim' does not exist on type 'number'.
}

value가 number일 수도 있으니 trim을 보장할 수 없습니다. 여기서 as string으로 우회하는 대신, typeof로 분기하면 컴파일러가 알아서 타입을 좁혀줍니다.

function format(value: string | number): string {
  if (typeof value === 'number') {
    // 이 분기 안에서 value는 number로 좁혀진다.
    return value.toFixed(2);
  }
  // 여기서는 자동으로 string으로 좁혀진다.
  return value.trim();
}
 
console.log(format(3.14159)); // 3.14
console.log(format('  hi  ')); // hi

typeof는 원시 타입을 좁힐 때 쓰고, 객체라면 ininstanceof가 있습니다. in은 속성이 있는지로, instanceof는 어떤 클래스의 인스턴스인지로 좁힙니다.

interface Admin {
  role: 'admin';
  permissions: string[];
}
interface Guest {
  role: 'guest';
  expiresAt: number;
}
 
function describe(user: Admin | Guest): string {
  if ('permissions' in user) {
    // permissions 속성을 가진 쪽, 즉 Admin으로 좁혀진다.
    return `관리자(${user.permissions.length}개 권한)`;
  }
  // Guest로 좁혀진다.
  return `게스트(만료 ${user.expiresAt})`;
}
 
function dump(value: Date | string): string {
  if (value instanceof Date) {
    // Date로 좁혀져 getTime을 안전하게 쓸 수 있다.
    return String(value.getTime());
  }
  return value;
}
 
console.log(describe({ role: 'admin', permissions: ['read', 'write'] })); // 관리자(2개 권한)
console.log(dump(new Date(0))); // 0

세 연산자는 런타임에 값을 실제로 확인하고, 컴파일러는 그 결과를 타입에 반영합니다. 단언과 달리 타입 검사를 건너뛰지 않습니다.

사용자 정의 타입 가드: param is T

typeofin만으로 좁히기 애매한 경우도 있습니다. 판별 로직이 길거나, 여러 곳에서 같은 검사를 재사용하고 싶을 때입니다. 이럴 때 사용자 정의 타입 가드를 만듭니다. 반환 타입을 param is T 형태로 선언하는 함수입니다.

interface Cat {
  type: 'cat';
  meow(): string;
}
interface Dog {
  type: 'dog';
  bark(): string;
}
 
// 반환 타입이 'animal is Cat' 이다.
// 이 함수가 true를 반환하면 호출한 쪽에서 animal은 Cat으로 좁혀진다.
function isCat(animal: Cat | Dog): animal is Cat {
  return animal.type === 'cat';
}
 
function speak(animal: Cat | Dog): string {
  if (isCat(animal)) {
    // 타입 가드 덕분에 animal은 Cat으로 좁혀진다. 단언이 필요 없다.
    return animal.meow();
  }
  return animal.bark();
}

isCat은 평범한 함수처럼 boolean을 반환하지만, 반환 타입을 animal is Cat으로 적었기 때문에 컴파일러는 이 함수가 true를 주면 인자를 Cat으로 간주합니다. 검사 로직을 함수 하나로 묶어두고, 호출하는 쪽은 좁혀진 타입을 그대로 받습니다.

DOM API에도 같은 방법을 씁니다. getElementById의 반환 타입은 HTMLElement | null이라 바로 value에 접근할 수 없고, 이때 as HTMLInputElement로 단언하기 쉽습니다.

// as HTMLInputElement로 단언하는 대신, 타입 가드로 검증한다.
function isInput(el: HTMLElement | null): el is HTMLInputElement {
  return el instanceof HTMLInputElement;
}
 
function readValue(id: string): string {
  const el = document.getElementById(id);
  if (isInput(el)) {
    // el은 HTMLInputElement로 좁혀져 value에 안전하게 접근한다.
    return el.value;
  }
  return '';
}

as를 쓴 버전은 요소가 input이 아니어도 컴파일을 통과하고, 런타임에 el.valueundefined가 되는 식으로 잘못 동작합니다. 타입 가드 버전은 instanceof로 실제 값을 확인하므로, input이 아닌 경우를 코드에서 직접 처리해야 합니다.

as가 필요한 경우

as를 쓰면 안 되는 것은 아닙니다. 컴파일러는 알 수 없지만 개발자는 문맥상 타입을 확신하는 경우가 있습니다. 테스트에서 일부 속성만 채운 목 객체를 만들 때나, 라이브러리 타입 정의가 실제보다 느슨할 때가 그렇습니다.

이때도 두 가지를 기억하면 좋습니다. 첫째, as는 런타임에 아무것도 검사하지 않으므로 단언이 틀려도 막을 방법이 없습니다. 둘째, as를 쓰기 전에 typeof나 타입 가드로 바꿀 수 있는지 먼저 확인합니다. 좁히기로 표현할 수 있는 검사는 좁히기로 두는 편이 안전합니다.

참고로 as const는 여기서 말하는 타입 단언과 목적이 다릅니다. as const는 값을 리터럴 타입으로 고정하는 용도라 타입 검사를 건너뛰지 않습니다.

정리

이번 글에서는 as를 줄이고 타입 좁히기로 안전하게 타입을 다루는 방법을 살펴봤습니다.

방법검사 시점특징
as 단언없음컴파일러 검사를 건너뛴다. 틀리면 런타임 에러가 난다
typeof런타임원시 타입(string, number 등)을 좁힌다
in런타임속성 존재 여부로 객체 타입을 좁힌다
instanceof런타임클래스 인스턴스인지로 좁힌다
사용자 정의 타입 가드런타임param is T로 판별 로직을 재사용 가능하게 묶는다
  • as는 값을 변환하지 않고 컴파일러 검사만 건너뛴다. 단언이 틀리면 런타임 에러가 난다.
  • typeof, in, instanceof는 값을 실제로 확인해 타입을 좁히므로 검사를 건너뛰지 않는다.
  • 판별 로직이 복잡하거나 재사용이 필요하면 param is T 타입 가드로 묶는다.

마치며

as로 지우던 타입 에러는 대부분 타입 좁히기로 해결할 수 있습니다. 다음 글에서는 neverunknown을 다루겠습니다. 긴 글 읽어주셔서 감사합니다.