타입 단언

타입 스크립트는 타입 표기, 가드, 추론 등의 기법으로 값의 타입을 판단한다. 하지만 떄론 컴파일러의 정보를 무시하고 개발자가 원하는 임의의 타입을 할당하고 싶을 수 있다. 이럴 때 사용하는 것이 타입 단언(type assertion)이다.

타입 단언을 사용하는 이유

a에 10이라는 값을 할당하면 타입스크립트는 알아서 number 타입으로 추론하게 된다. 따라서, 변수 b에 a를 할당하면 b역시 number타입을 갖게 되는 것이다.

image

하지만, a를 최초 선언 당시 아무 값도 할당하지 않았으므로, 타입스크립트는 a를 any 타입으로 추론하게 된다. 따라서, b역시 any 타입으로 추론하는 것이다.

image

하지만, 개발자들은 마지막 ‘abc’를 할당했으므로 string 타입을 기대했을 것이다. 이럴 때 사용하는 것이 바로 타입 단언(type assertion)이다 타입스크립트보다 개발자가 그 타입에 대해 정확하게 알고 있을 때 사용한다고 생각하면 된다. value as Type 문법을 사용해서 value를 type으로 단언할 수 있다.

image

주의할 점

타입 단언은 타입 에러를 없애줄 분 런타임 에러를 막아주지 못한다. 오히려 그 반대인데, 컴파일타입에서 잡을 수 있는 에러를 없앰으로서 원래대로라면 발생하지 않을 런타임 에러를 발생시킬 수 있다. 아래의 코드를 보자.

image

유니온 타입은 공통타입이 아닌 타입에 대해서는 에러를 발생시킨다. weight속성은 dog 타입에만 존재하므로 dog 타입으로 단언하면 타입에러를 없앨 수 있다.

image

하지만, 아래와 같이 예상되지 않는 타입의 값이 들어가면 에러가 발생한다.

interface dog {
  name: string;
  weight(): void;
}

interface cats {
  name: string;
  age: number;
}

function test (info: dog | cats){
  return (info as dog).weight();
}

var info = {
  name: 'siksik',
  age: 10
}

test(info)

image


타입 가드

특정 스코프 내에서 값의 타입을 좁혀나가는 것을 타입 가드라 한다.

타입 가드는 크게 두 종류로 나뉜다.

  • 제어 흐름 분석(control flow analysis)
  • 사용자 정의 타입 가드(user defined type guard)

제어 흐름 분석(control flow analysis)

기본적으로 자바스크립트는 비동기 실행 코드를 제외하고는 전부 위에서부터 아래로 순차적으로 코드가 실행된다. 대부분의 프로그래밍 언어는 특정 조건이 만족될 때에만 코드를 실행하거나 같은 코들르 여러번 실행하는 식으로 순차적 실행을 벗어난 실행을 가능하게 하는 제어 구조(control structure)를 제공한다.

자바스크립트와 타입스크립트 또한 제어 구조를 제공한다. 대표적인 제어 구조는 아래와 같다.

  • if, else if, else
  • while, for
  • switch, case
  • break, continue
  • return

컴파일러는 위의 제어 구조로부터 특정 시점에서의 상태 정보를 얻는다. 그리고 컴파일러는 이러한 정보를 이용해 제어 흐름 분석을 진행하여 특정 값의 타입을 좁혀낼 수 있다. 그렇다면, 제어 흐름 분석으로 어떻게 타입을 좁히는지 알아보자.

undefined / null 비교

undefined 또는 null 과의 비교로 각각 대응하는 타입에 대한 타입 가드로 동작한다.

interface info {
  name: string | null;
}

function person(info: info){
  if(info.name === null){
    return 'null'
  }else {
    // info.name 타입은 string
    return info.name;
  }
}

리터럴 타입 비교

리터럴 타입과의 비교도 타입 가드로 동작한다.

interface Human {
  type: 'human';
  sex: 'man';
}

interface Animal {
  type: 'animal';
  name: 'dog'
}

function something(data: Human | Animal){
  switch (data.type) {
    case 'human': {
      // data는 Human 타입
      return data.sex;
    }
    case 'animal': {
      // data는 Animal 타입
      return data.name;
    }
    default: {
      // data는 null 타입
      return null;
    }
  }
}

공통 리터럴 타입인 type을 기반으로 switch-case를 통해 각 브랜치에서 타입을 좁힐 수 있다.

typeof 연산자 비교

typeof 연산자는 하나의 인자를 받아 해당 인자의 타입을 문자열로 반환한다. 이를 통해 typeof 반환값과 문자열을 비교한 결과를 타입 가드로 사용할 수 있다.

function something(data: string | number | boolean){
  if(typeof data === 'string'){
    return 'string type'
  }

  if(typeof data === 'number'){
    return 'string type'
  }

  if(typeof data === 'boolean'){
    return 'string type'
  }

  throw new Error('string or number or boolean 이외의 타입이 들어왔습니다.')
}

typeof 연산자를 사용할 때는 프로그래머의 예상과 다르게 동작한다는 것을 주의해야한다. 흔히 사용되는 타입과 typeof 연산자의 반환값을 아래의 표로 확인해보자.

하위 호환성 이슈로 typeof null은 “null”이 아닌 “object”로 반환된다. 또한 typeof [] === “array”일 것이란 예상과 달리 배열을 나타내는 별도의 반환값이 존재하지 않는다. 이러한 혼란스러움으로 typeof로 타입 가드를 사용할 때는 number, string, boolean, symbol과 같이 단순한 타입에 대해서만 사용하는 것을 권장한다.

타입 typeof 반환값
Undefined “undefined”
Null “object”
Boolean “boolean”
Number “number”
String “string”
Symbol “symbol”
Function “function”
그 외 모든 객체 “object”

instanceof 연산자 비교

instanceof 연산자는 값과 생성자를 받아 해당 값의 프로토타입 체인에 해당 생성자가 있는지 확인한다. ES6 클래스는 내부적으로 프로토타입 체인에 기반해 돌아가기 때문에, 클래스의 인스턴스 여부도 instanceof를 이용하여 타입 가드할 수 있다.

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) { }

  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");

  }
}

class StringPadder implements Padder {

  constructor(private value: string) { }

  getPaddingString() {
    return this.value;
  }
}


function getRandomPadder() {
  return Math.random() < 0.5 ?
    new SpaceRepeatingPadder(4) :
    new StringPadder("  ");
}


// 이 시점에선 'SpaceRepeatingPadder | StringPadder'
let padder: Padder = getRandomPadder();


if (padder instanceof SpaceRepeatingPadder) {
  console.log(padder); // SpaceRepeatingPadder 로 좁혀짐
}


if (padder instanceof StringPadder) {
  console.log(padder); // StringPadder 로 좁혀짐
}

in 연산자 비교

in 연산자는 객체에 특정 속성이 존재하는지 여부를 판단하는데 사용한다.

interface Human {
  name: string;
}

interface Animal {
  age: number;
}


type UnionType = Human | Animal;

function something(data: UnionType) {
  if('name' in data){
    // Human Type
    console.log(data.name);
  }else{
    // Animal Type
    console.log(data.age)
  }
}

사용자 정의 타입 가드

지금까지 타입스크립트 언어에 내장된 제어 흐름에 기반하여 동작하는 타입 가드를 살펴봤다. 이 뿐만 아니라 직접 임의의 기준을 사용해 타입 가드를 정의할 수 있는데 이것을 사용자 정의 타입 가드라 한다. 사용자 정의 타입 가드는 value is Type 형태로 반환 타입을 갖는 함수로 정의한다.

예를 들어, 위의 in연산자를 통해 타입가드를 사용했다면 특정 Human 타입인지 확인하는 isHuman() 함수로 대체할 수 있다.

interface Human {
  name: string;
}

interface Animal {
  age: number;
}


type UnionType = Human | Animal;

function isHuman(data: UnionType): data is Human {
  return (data as Human).name !== undefined;
}

function something(data: UnionType) {
  if(isHuman(data)){
    // Human Type
    console.log(data.name);
  }else{
    // Animal Type
    console.log(data.age)
  }
}


something({name:'siksik'});
something({age:10});