본문 바로가기

프로그래밍/TypeScript

09. 고급 타입

09. 고급 타입

9.1 유니언 타입과 타입 가드

유니언 타입은 여러 타입을 받을 수 있다는 장점이 있습니다.
하지만, 여러 타입을 받음으로서 타입을 확신할 수 없다는 문제도 있습니다.
따라서 매개변수가 유니언 타입일 때 안전한 값을 할당하려면
타입 검사를 거쳐야 합니다.
이처럼 유니언 타입에 대해 타입검사를 통해 타입 안정성을 주는 방법을 타입가드 라고 합니다.
타입 가드(type guards)

typeof나 instanceof 연산자를 사용해 타입 질의를 한 후 명시된 타입과 일치하는지 검사합니다. 

---------------------------------------------------------
//union-typeof.ts
function myIndexOf(x: number | string, y: string) {
    if (typeof x === "string") {
        return x.indexOf(y);
    } else {
        return -1;
    }
}
console.log(myIndexOf("hello", "e"));

// union-instanceof.ts
class Cat {
    name = "cat";
    age = 13;
}
class Dog {
    name = "dog";
    leg = 4;
}
function diffCheck(x: Cat | Dog) {
    if (x instanceof Dog) {
        console.log(x.name);
        console.log(x.leg);
    }
    console.log(x.name);
    // console.log(x.leg);
    // console.log(x.age);
}
diffCheck(new Dog());

---------------------------------------------------------

타입 안전성을 확보한 경우에는 Dog class 의 name과 leg에 접근할 수 있지만, 
조건 검사가 이루어지지 않은 영역에서는 Cat과 Dog에 공통으로 선언된 name 변수를 제외하고는
오류가 발생합니다. 


9.2 알아두면 쓸모있는 고오급 타입들

9.2.1 문자열 리터럴 타입
문자열 리터럴 타입은 타입에 정의한 문자열만 할방받을 수 있게 하는 사용자 정의 타입입니다. 

---------------------------------------------------------
type EventType = "keyup" | "mouseover";

const myEvent: EventType = "keyup";
console.log(typeof myEvent, myEvent);

function on(event: EventType, callback: (message: string) => any) {
    console.log(typeof event, event);
    callback("callback!");
}
on(myEvent, (message) => console.log(message));
---------------------------------------------------------

이처럼 문자열 리터럴 타입을 사용하면 매개변수에 허용가능한 문자열을 제한할 수 있습니다.

9.2.2 룩업 타입

룩업타입은 인덱스 접근 타입으로 불립니다.
keyof를 통해 타입 T의 하위 타입을 생성해내기 때문입니다. 
여기서 타입 T는 유니언이나 인터페이스 타입을 말합니다.

let testUnion: "name" | "gender" | "age" = "name";

---------------------------------------------------------
interface Profile {
    name: string;
    gender: string;
    age: number;
}

type Profile1 = keyof Profile;
let pValue1: Profile1 = "name";
pValue1 = "gender";
pValue1 = "age";
// pValue1 = "name2";

/*
룩업 타입으로 선언한 변수는 Profile 인터페이스의
속성 이름 중 하나를 할당받을 수 있습니다. 
name2 는 인터페이스에 일치하는 속성이 없으므로 오류가 발생합니다.
*/

type Profile2 = keyof Profile[];
let pValue2: Profile2 = "length";
pValue2 = "push";

/*
Profile2 는 배열 타입의 내장 속성인 length, push, pop, concat 등을 
할당해 사용할 수 있습니다.
*/

type Profile3 = keyof { [x: string] : Profile };
let pValue3: Profile3 = "hello";
pValue3 = "miso";

/*
Profile 타입으로 정의된 익명 배열요소 [x: string] 을 keyof 로 가져옵니다.
여기서 [x: string]은 익명배열 요소의 문자열 타입이므로
어떤 문자열이든 입력할 수 있습니다.
*/

type Profile4 = keyof Profile["name"];
let pValue4: Profile4 = "length";
// pValue4 = "abc";

/*
Profile4 는 keyof 로 string 타입을 전달했습니다.
따라서 타입이 string 일 때 접근 가능한 내장속성을 이용할 수 있습니다. 
*/

---------------------------------------------------------

9.2.3 non-nullable

- non-nullable 타입의 등장 배경
TS 2.0 이전에는 모든 타입의 변수에 null이나 undefined를 할당할 수 있었습니다.
하지만 이것은 타입의 모호성을 일으킬 수 있기 때문에 
TypeScript 2.0에서는 strictNullCheck 라는 옵션이 추가됐습니다.

만약 strictNullCheck가 true 일때 null 또는 undefined를 사용하고 싶다면 
유니언 타입을 사용하면 됩니다.

---------------------------------------------------------
let title: string | null;
title = "TypeScript Programming!";
title = null;

let title2: string | undefined;
title2 = "TypeScript Programming!!";
title2 = undefined;
---------------------------------------------------------

9.2.4 네버타입

Never 타입은 모든 타입의 하위 타입으로 쓸 수 있지만, any 타입을 never 타입에 할당할 수는 없습니다.

never 타입을 사용하는 경우
1) 함수에 닿을 수 없는 코드 영역이 있어 반환값이 존재하지 않을 때
2) 함수에 Throw 가 반환되어 오류가 발생할 때

1) 닿을 수 없는 코드 (unreachable code)
---------------------------------------------------------
const neverTouch = function(): never {
    while(true) {
        console.log("Never");
    }
    // console.log("");
}
let resultNever: never = neverTouch();
---------------------------------------------------------

---------------------------------------------------------
function neverTest(value: string | number) {
    if (typeof value === "string") {
        return value;
    } else if (typeof value === "number") {
        return value;
    } else {
        return value;
    }
}
console.log(neverTest("test"));
---------------------------------------------------------

neverTest함수의 매개변수로 유니언 타입을 지정해 string 또는 number 타입만 전달할 수 있는데, 
조건 검사에서 모두 처리하므로 else 는 닿을 수 없는 코드가 됩니다.

이처럼 닿을 수 없는 코드를 없애려면 조건 검사에서 예외 상황이 없도록 수정하면 됩니다.

---------------------------------------------------------
if (typeof value === "string") {
    return value;
} else {
    return value;
}
---------------------------------------------------------


2) 예외 객체가 반환될 때

---------------------------------------------------------
function error(message: string): never {
    throw new Error(message);
}
function fail() {
    return error("Error!!");
}
fail();
---------------------------------------------------------

이 예제를 실행해보면 throw new Error(message)를 실행함으로써 예외가 발생했습니다.
error 함수가 오류를 반환하면 어떠한 값이 반환되는 것이 아니므로 네버 타입이 됩니다.

반대로 오류가 발생하지 않는 함수에 네버타입을 쓰면 
"never를 반환하는 함수에는 닿을 수 있는 종료지점이 있으면 안됩니다" ? 라는 오류 메시지가 나타납니다.

즉, 필요한 상황에서만 써라.


9.2.5 this 타입

this 타입은 인터페이스와 클래스의 하위 타입이면서 이들을 참조할 수도 있는 타입입니다. 
this 타입을 다형적 this 타입 (polymorphic this type) 이라고도 하는데, 선언 위치에 따라 참조하는 대상이 달라지기 때문입니다. 

클래스의 멤버 변수나 생성자에서 this 타입을 사용하면
가장 가까운 클래스의 인스턴스를 참조합니다.
인터페이스 멤버에서 this 타입을 사용하면 인터페이스를 참조합니다.

interface ListItem {
    getHead(): this;
    getTail(): this;
}

인터페이스의 멤버는 자기 자신을 참조합니다.
인터페이스는 구현 클래스의 객체를 통해 getHead()를 호출하면, 자기자신을 다시 호출하는
재귀 타입(recursive type)이 됩니다. 

- 체이닝 (chaining)

new MyCalc(1).multiply(5).add(10);

이처럼 자기자신(self)를 반환하는 인스턴스 메소드를 활용해 체이닝 형태로 선언하면 
마치 데이터가 흐르는 듯이 표현할 수 있는데 이러한 패턴을
fluent interface pattern (플루언트 패턴) 이라고 합니다.

플루언트 패턴을 구현할 때는 메서드가 인스턴스 자신을 가리키는 this 타입을 반환합니다. 

---------------------------------------------------------
class AddCalc {
    public constructor(public value: number = 0) { }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
}
class MyCalc extends AddCalc {
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
}

// 3 * 5 + 10 = 25
let calc = new MyCalc(3).multiply(5).add(10);
console.log(calc.value);
---------------------------------------------------------

부모 클래스에 this 타입을 반환하는 add 메소드를 추가하고, 
자식 클래스에 this 타입을 반환하는 multiply 메소드를 추가합니다.

상위 클래스에 선언된 value 는 number 로 선언됐고,
add 와 multiply는 this 타입을 반환합니다.
this 타입은 선언된 위치에서 가장 가까운 클래스의 인스턴스를 참조합니다.

따라서 add 메소드는 AddCalc 클래스의 인스턴스를 참조하고, 
multiply 메소드는 MyCalc 클래스의 인스턴스를 참조합니다.

그래서 객체를 생성하고 체이닝 형태로 메소드를 연결해 실행할 수 있습니다.

multiply() 메소드의 반환형을 this 타입이 아닌 number 로 변경할 경우 
multiply 메소드는 더이상 MyCalc 클래스를 참조할 수 없기때문에
더이상 체이닝 메소드를 추가할 수 없게 됩니다. 

'프로그래밍 > TypeScript' 카테고리의 다른 글

11. 제네릭  (0) 2019.10.23
10. 타입선언과 변경, 호환  (0) 2019.10.23
08.모듈  (0) 2019.10.16
07.클래스와 인터페이스 기반의 다형성  (0) 2019.10.15
07. 클래스와 인터페이스  (0) 2019.10.15