union type인 객체를 사용할떄 공통으로 가지고 있는 프로퍼티가 아닌 프로퍼티를 접근한 경우 typescript 에서 오류를 발생시킨다. 해당 객체가 실제로 어떤 타입일지 알 수 없기 때문에 발생하는 현상인데(tsc입장에서는 당연한 스펙) 아래에 설명할 type guard를 사용하여 안전하게 접근이 가능하다.

예제 코드

function getNumberOrString(): number | string {
    if (Math.random() < 0.5) {
        return 100;
    } else {
        return "string";
    }
}

let numOrStr = getNumberOrString();
numOrStr.toString(); // 문제 없음
numOrStr.toUpperCase(); // 오류, toUpperCase가 없을 가능성이 있음 

아래의 내용들은 공식 문서의 설명을 간단히 정리한 내용이다.

type assertion으로 우회

type assertion으로 프로퍼티 접근을 가능하게 만든후 실제로 프로퍼티가 있는지 테스트를 한 후 실제 프로퍼티에 접근한다. . 구문이 복잡하고 영 깔끔해 보이지 않으므로 아래에 소개되는 진짜 type guard를 쓰는 것이 바람직하다.

if ((numOrStr as string).toUpperCase) {
    (numOrStr as string).toUpperCase();
}

type guard

공식 문서의 설명을 옮기자면 특정 스코프에서 타입을 보장하도록 런타임 체크를 수행하는 문법이라고 한다.

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

Using type predicates

parameterName is Type형태의 type predicate를 리턴 타입으로 하는 함수를 만들고 타입체크 결과를 boolean으로 리턴하도록 한다. 이 함수가 true인 if 분기의 스코프에서는 타입을 보장 받는다.

function isString(target: number | string): target is string /* type predicate */ {
    return typeof target == 'string'; // 사실 boolean을 리턴한다
}

if (isString(numOrStr)) {
    // 여기 스코프 안에서는 string이라고 보장
    numOrStr.toUpperCase();
}

함수를 바로 사용하지 않고 변수에 할당후 사용할 경우 단순 boolean 변수로 type guard가 아니다

const resultOfIsString = isString(numOrStr);
if (resultOfIsString) {
    numOrStr.toUpperCase(); // 오류 발생
}

Using the in operator

in operator의 true 분기에서는 해당 속성이 있는것을 보장하고 false 분기에서는 없는것을 보장 한다고 문서에 적혀있다. in으로 검사한 속성만 쓸수 있을꺼 같지만 타입으로 해석 하여 타입내 다른 속성도 접근 가능하다. (아래 테스트 참고)
다만 primitive에 대해서는 사용이 불가능하고 any, object type, type parameter에 한해서 사용할 수 있다.

interface A {
    a();
    aa();
}

interface B {
    b();
    bb();
}

function getAOrB(): A | B {
    if (Math.random() < 0.5) {
        return {
            a: () => {},
            aa: () => {},
        };
    } else {
        return {
            b: () => {},
            bb: () => {},
        };
    }
}

const aOrB = getAOrB();
if ("a" in  aOrB) {
    aOrB.a(); // in 체크 성공으로 오류 없음
}

if ("a" in  aOrB) {
    aOrB.aa(); // a만 체크했지만 타입이 A임을 알기 때문에 오류 없음
}

if ("a" in  aOrB) {
    aOrB.b(); // 얘는 당연히 안된다
}
let numOrStr = getNumberOrString();

if ('toUpperCase' in numOrStr) { // 여기서 오류, "The right-hand side of an 'in' expression must be of type 'any', an object type or a type parameter."
    numOrStr.toUpperCase();
    numOrStr.toLowerCase();
}

typeof type guards

typeof를 아래 두가지 형태로 사용하여 가드로 사용할 수도 있다.

  • typeof v === "typename"
  • typeof v !== "typename"

다만 여기서도 제약이 있는데 typeof로 체크가 가능한 number, string, boolean, symbol에만 쓸 수 있다. 인터페이스나 클래스는 typeof로 확인이 안되기 때문에 당연한 스펙으로 봐야겠다.

if (typeof numOrStr === 'string') {
    numOrStr.toUpperCase(); // 통과
}

if (typeof numOrStr == 'string') {
    numOrStr.toUpperCase(); // 이것도 통과
}

instanceof type guards

typeof 에서 구분할 수 없는 object형에 대한 클래스 가드는 instanceof로 확인 할 수 있다.

class A {
    a() {}
    aa() {}
}

class B {
    b() {}
    bb() {}
}

function getAOrB(): A | B {
    if (Math.random() < 0.5) {
        return new A();
    } else {
        return new B();
    }
}

const aOrB = getAOrB();
if (aOrB instanceof A) {
    aOrB.a();  // 통과
    aOrB.aa(); // 통과 
}

if (aOrB instanceof A) {
    aOrB.b();  // 오류 
}