고급 타입

레퍼런스
유니온 타입

문제상황: 리턴하는 타입이 string일 수도 Error일 수도 있는데..
어찌해야합니까?

/**
 * string 타입을 가져와서 왼쪽에 "padding"을 추가합니다.
 * 'padding'이 string인 경우에는 'padding'이 왼쪽에 추가됩니다.
 * 'padding'이 number인 경우에는 해당 개수의 공백이 왼쪽에 추가됩니다.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // "    Hello world" 반환

any를 쓸거면 왜 typescript를 쓰겠어요?
type을 제한해서 컴파일시 에러를 발생시키자는게 typescript를 쓰는 이유중 하나일 것 같아요.

let indentedString = padLeft("Hello world", true); // 컴파일 타임에는 통과되지만 런타임에는 실패합니다.

전통적인 객체 지향 코드에서는 타입의 계층을 만들어 두 가지 타입을 추상화 할 수 있습니다.
이것이 훨씬 더 명백하기는 하지만 또 약간 지나치기도 합니다.

즉 두 가지 타입을 묶어서 쓸 수 있지만, 굳이 그래야 하나입니다.

그때 사용하는 것이 바로 유니온 타입입니다.

유니온 타입은 여러 타입 중 하나일 수 있는 값을 설명합니다.
각 타입을 구분하기 위해 수직 막대(|)를 사용하므로 number | string | boolean은 number, string 또는 boolean 될 수 있는 값의 타입입니다.

/**
 * string 타입을 가져와서 왼쪽에 "padding"을 추가합니다.
 * 'padding'이 string인 경우에는 'padding'이 왼쪽에 추가됩니다.
 * 'padding'이 number인 경우에는 해당 개수의 공백이 왼쪽에 추가됩니다.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // 컴파일 시 오류

또한, 유니온 타입이 있는 값이 있으면 유니온의 모든 타입에 공통적인 멤버에만 접근할 수 있습니다.

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // 좋아요
pet.swim();    // 오류

swim은 공통적으로 갖고 있는 메서드가 아니잖아요. Fish 타입에만 있으니까요.

타입 호환성

레퍼런스
TypeScript의 구조 타입 시스템에 대한 기본적인 규칙은 y가 적어도x와 같은 멤버를 가지고 있다면 x는 y와 호환된다는 것입니다.

interface Named {
    name: string;
}

let x: Named;
// y의 추론된 타입은 { name: string; location: string; } 입니다
let y = { name: "Alice", location: "Seattle" };
x = y;

x = y에 집중.
y가 x가 할당될 수 있는 지를 검사하기 위해서는 컴파일러는 x의 각 프로퍼티를 검사하여 y에 상응하는 호환되는 프로퍼티를 찾습니다.

y에 문자열인 name 멤버를 가져야합니다. 그렇기 때문에 할당이 허용됌.

함수 호출 매개인자를 검사할때는

function greet(n: Named) {
    alert("Hello, " + n.name);
}
let y = { name: "Alice", location: "Seattle" };
greet(y); // 좋아요

호환성을 검사할 때 대상의 타입 ( 위 경우에는 Named) 멤버만 고려함.
즉, Named 타입에는 문자열 name 멤버가 있고,
y객체에도 문자열 name 멤버가 있으니 매개인자 할당이 가능하게 됌.

타입 단언

레퍼런스: https://hyunseob.github.io
타입단언을 왜 쓰는지에 대해서는 다음 예제를 보고 이 예제를 어떻게 해결하는지 과정을 통해 알 수 있습니다.

컴파일 에러 예제

class Character {
  hp: number;
  runAway() {
    /* ... */
  }
  isWizard() {
    /* ... */
  }
  isWarrior() {
    /* ... */
  }
}

class Wizard extends Character {
  fireBall() {
    /* ... */
  }
}

class Warrior extends Character {
  attack() {
    /* ... */
  }
}

function battle(character: Character) {
  if (character.isWizard()) {
    character.fireBall(); // Property 'fireBall' does not exist on type 'Character'.
  } else if (character.isWarrior()) {
    character.attack(); // Property 'attack' does not exist on type 'Character'.
  } else {
    character.runAway();
  }
}

이 코드는 컴파일 에러를 내는게 당연하죠. Character 클래스에는 fireBall, attack 메소드가 선언조차 되어있지 않으니까요.
isWizard라는 메소드를 통해 확실히 그 캐릭터가 Wizard 인스턴스라는 걸 보장할 수 있다면, if 블록 안에서는 당연히 fireBall이라는 메소드를 사용할 수 있어야 합니다.

컴파일 에러가 없애버려면 다음과 같이 되야합니다.
이 때, 타입 단언으로 적절한 타입을 다시 선언해줄 수 있습니다.

function battle(character: Character) {
  if (character.isWizard()) {
    (character as Wizard).fireBall(); // Pass
  } else if (character.isWarrior()) {
    (character as Warrior).attack(); // Pass
  } else {
    character.runAway();
  }
}

character as Wizard <- 요 형태가 타입 단언 type assertion입니다.

그리고, 주의할 것은
해당 변수가 실제로 Wizard 인스턴스가 아니더라도 as 키워드를 통해서 타입 단언을 해줄 수 있기 때문에, 타입 단언은 주의해서 사용해야 한답니다.

실제로도 as any 라는 치트키로 대부분의 컴파일 에러를 해결할 수 있대요.

그러나, typescript를 사용하는 목적은 타입체크를 컴파일때 하길 원하기 때문에~ as와 any는 가능한 적게 사용하는 것이 좋다!라는 것입니다.

아직도 타입 단언이라? 아리송하다면 다른 레퍼런스로~
레퍼런스: TypeScript-Handbook 한글 문서

타입 단언를 사용하는 방법은 두 가지입니다. 여기선 그 중 as 키워드를 방법을 썼습니다.

타입 단언이 타입 캐스팅이 아닌 이유

타입 단언은 타입을 변경한다는 사실 때문에 타입 캐스팅과 비슷하게 느껴질 수 있다.
타입 단언이 타입 캐스팅이라고 불리지 않는 이유는 런타임에 영향을 미치지 않기 때문이다.
타입 캐스팅은 컴파일타임과 런타임에서 모두 타입을 변경시키지만 타입 단언은 오직 컴파일타임에서만 타입을 변경시킨다.

타입캐스팅은 (type)variable 로 많이 사용해왔어요.


이제 타입 가드에 대해서 알아볼게요.
다음 예제를 볼게요.

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();

// 이러한 각 프로퍼티 접근은 오류를 발생시킵니다.
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

타입 단언을 알기 때문에 이제는 타입 단언을 이용해서 위 코드가 동작하게 바꿀 수 있어요.

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (pet as Fish).swim();
}
else {
    (<Bird>pet).fly();
}

타입 단언을 여러번 사용해야만 했어요.
각 지점에서 pet 타입을 알 수 있습니다.

typescript에서는 이럴때 타입 가드를 쓰면 코드 퀄리티가 높아져요.
타입 가드(type guard)는 일부 스코프에서 타입을 보장하는 런타임 검사를 수행하는 표현식입니다.
레퍼런스 : TypeScript-Handbook 한글 문서 - 고급 타입 - 사용자 정의 타입 가드

타입 가드

타입을 검사하는 메서드를 만들어서 명시적으로 타입을 검사하자라는 방식인것 같습니다.

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
//  return (<Fish>pet).swim !== undefined; 둘다 가능합니다.
}

pet is Fish는 이 예제에서 타입 명제입니다.
명제는 parameterName is Type 형태을 취합니다, 여기서 parameterName은 현재 함수 시그니처의 매개 변수 이름이어야 합니다.
IsFish가 일부 변수와 함께 호출될 때 원래 타입이 호환 가능하다면 TypeScript는 그 변수를 특정 타입으로 제한할 것입니다.

IsFish가 일부 변수와 함께 호출될 때 원래 타입이 호환 가능하다면 TypeScript는 그 변수를 특정 타입으로 제한할 것입니다.

// 'swim'과 'fly' 호출은 이제 모두 괜찮습니다.
if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

TypeScript는 pet이 if 스코프의 Fish라는 것을 알고 있을 뿐만 아니라,
else 스코프에서는 Fish가 없다는 것을 알기 때문에 Bird가 있어야 합니다.

보다 자세한 내용은 아래 레퍼런스의 고급타입에서 타입 가드 키워드로 검색해보시면 나옵니다.
레퍼런스 : TypeScript-Handbook 한글 문서

타입체크

레퍼런스: TypeScript-Handbook 한글 문서 - 고급 타입 - type 타입가드
자바스크립트에서 타입체크하는 방법은

크게 typeof v === "typename"
"typename"은 반드시 "number", "string", "boolean", 또는 "symbol" 옵니다.

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

그리고 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) {
    padder; // 타입이 'SpaceRepeatingPadder'로 좁혀졌습니다

if (padder instanceof StringPadder) {
    padder; // 타입이 'StringPadder'로 좁혀졌습니다.
}

instanceof의 오른쪽에는 생성자 함수가 있어야 하며 TypeScript는 다음과 같이 범위를 좁혀 나갑니다:

타입이 any가 아닌 경우 함수의 prototype 프로퍼티 타입
해당 타입의 생성자 시그니처에 의해 반환된 타입의 결합
이와 같은 순서로 진행됩니다.

instanceof란?
instanceof 연산자는 생성자의 prototype 속성이 객체의 프로토타입 체인 어딘가 존재하는지 판별합니다.