타입스크립트 고급 타입

타입스크립트에서 더 알아보기

Intro


고급 타입


실제 프로젝트에서 유용하게 사용할 수 있도록 기존 개념에서 더 알아가보도록 하자. 설명할 타입들은 실제로 프로젝트에서 자주 사용되므로 숙지하도록하자.

인터섹션(교차) 타입


인터섹션 타입은 다른 타입을 결합할 수 있는 타입이다. 이때 사용하는 키워드는 &이다. 타입 객체1 & 타입 객체2

ts
// Admin 명으로 사용자 정의 타입을 생성한다.
type Admin = {
    name: string;
    privileges: string[];
};

type Employee = {
    name: string;
    startDate: Date;
};

// 두 타입을 결합하여 새로운 정의 타입 객체를 생성한다.
type ElevatedEmployee = Admin & Employee;

// 결합된 두 사용자 타입내 속성을 정의한다.
const e1: ElevatedEmployee = {
    name: 'Lim',
    privileges: ['create-server'],
    startDate: new Date()
}

type Combinable = string | number;
type Numeric = number | boolean;

// 위의 두 유니언 타입을 결합한 유니언 타입을 새로 생성할 수 있다.
type Universal = Combinable & Numeric;

아래처럼 인터페이스로도 같은 결과를 도출할 수 있다.

ts
// Admin 명으로 사용자 정의 타입을 생성한다.
interface Admin = {
    name: string;
    privileges: string[];
};

interface Employee = {
    name: string;
    startDate: Date;
};

interface ElevatedEmployee extends Employee, Admin {
    // ...
}

타입 가드


타입 가드는 특히 더 자주 사용된다. 유니언 타입을 예로 유니언 타입은 유연성을 가지지만 런타임시엔 정확히 어떤 타입을 얻게 될지 알아야 하는 경우가 있다.

ts
type Combinable = string | number;
type Numeric = number | boolean;

type Universal = Combinable & Numeric;

function add(n1: Combinable, b: Combinable) {
    // return a + b; // Error!
    if(typeof a === 'string' || typeof b === 'string') {
        return a.toString() + b.toString();
    }
    return a + b;
}
  • 8번 라인을 타입 가드라고 한다. 유니온 타입이 지닌 유연성을 활용하며 런타임시 정확하게 작동하게 해준다.

in 타입가드


만일 두 사용자 타입을 유니온 타입으로 사용하는 경우 다른 한쪽에는 없는 속성을 사용하려는 경우 에러가 발생한다. 이때 in 타입가드 사용으로 타입스크립트가 감지하여 typeof기능을 대신해 코드 에러를 방지할 수 있다.

ts
type Admin = {
    name: string;
    privileges: string[];
};

type Employee = {
    name: string;
    startDate: Date;
};

type UnKnownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnKnownEmployee) {
    console.log('Name: ' + emp.name);
    // console.log('Previleges: ' + emp.privileges); // Property 'privileges' does not exist on type 'UnknownEmployee'.
                                                  // Property 'privileges' does not exist on type 'Employee' .
    if ('previleges' in emp) {
        console.log('Previleges: ' + emp.privileges);
    }
    if ('startDate' in emp) {
        console.log('StartDate: ' + emp.startDate);
    }
};

type ElevatedEmployee = Admin & Employee;

const e1: ElevatedEmployee = {
    name: 'Lim',
    privileges: ['create-server'],
    startDate: new Date()
}
    
printEmployeeInformation(e1);
>> Name: Lim
>> Privileges: create-server
>> StartDate: Wed Sep 25 2019 10:59:02 GMT+0200
  • 15~16번 라인의 경우 타입가드가 없다면 Admin만 privileges를 가지기 때문에 에러를 발생시킨다.
  • 사용자 정의 타입(type)의 경우 typeof를 통해 판별할 수 없다. 타입스크립트에만 존재하는 기능으로 자바스크립트에서는 컴파일할 수 없기 때문이다. 이때 in 타입가드를 이용한다.
  • 17~19번 라인에서 in 타입가드를 이용하여 타입스크립트가 감지하고 이를 확인해 if 문 내 속성으로 접근할 수 있게 해준다.

instanceof 타입 가드


instanceof 는 바닐라 JS에 내장된 일반 연산자이다. in과 달리 런타임시 실행되는 typeof와 같은 자바스크립트의 일부이다.

ts
class Car{
    drive() {
        console.log('Driving...');
    }
}

class Truck {
    drive() {
        console.log('Driving a truck...');
    }

    loadCargo(amount: number){
        console.log('Loading cargo ...' + amount);
    }
}

type Vehicle = Car | Truck;

const v1 = new Car();
const v2 = new Truck();

function useVehicle(vehicle: Vehicle) {
    vehicle.drive();
    if('loadCargo' instanceof Truck){
        vehicle.loadCargo(1000);
    }
}

useVehicle(v1);
useVehicle(v2);
  • 자바스크립트는 type객체는 모르지만 new생성자 함수는 알고 있으므로 클래스(1, 7번 라인)는 인스턴스화되어 객체로 변환되어 사용한다 타입스크립트는 Truck 인스턴스를 기반으로 Vehicle이 생성되는지 확인할 수 있다. (interface의 경우 타입스크립트 기능으로 예외.)
    때문에 자바스크립트가 Class와 생성자 함수(new)를 지원하므로 24~26번 라인 처럼 instanceof 연산자를 사용할 수 있다. 런타임시에 사용 가능하기 때문이다. instanceof로 비교 대상이 클래스 인지 확인할 수 있고 맞다면 해당 클래스의 메서드를 사용할 수 있다.

요약


타입가드는 특정 속성이나 메소드 사용 이전에 존재하는지를 확인하거나 타입 사용전 확인하는 개념 또는 방식을 나타내는 용어이다.

  • 객체의 경우 instanceof, in을 사용하며 런타임시 사용가능한 경우 instanceof를 이용한다.
  • 다른 타입(string, number, boolean 등.)의 경우 typeof를 사용할 수 있다.

구별된 유니온 타입


타입가드를 쉽게 구현하기 위해 구별된 유니온을 사용할 수 있다. 아래 구문을 보면 이번엔 클래스가 아닌 인터페이스를 사용한다. 인터페이스 이므로 in타입가드를 사용해야한다.

ts
interface Bird {
    type: 'bird'; // 'bird'값을 가진 리터럴 타입
    flyingSpeed: number;
}

interface Horse {
    type: 'horse'; // 'horse'값을 가진 리터럴 타입
    runningSpeed: number;
}

type Animal = Bird | Horse;

functuon moveAnimal(animal: Animal){
    let speed;
    // if('flyingSpeed' in animal) // 인터페이스를 사용하므로 in 키워드를 이용한다.
    switch(animal.type){
        case 'bird':
            speed = animal.flyingSpeed;
            break;
        case 'horse':
            speed = animal.runningSpeed;
    }
    console.log('Moving with spped ' + speed);
}

moveAnimal({type: 'bird', flyingSpeed: 10});

구별된 유니언은 다음과 같이 사용한다.

  • 2, 7라인과 같이 각 인터페이스에 값을 가진 리터럴 타입을 정의한다.
  • 22~27라인에서 switch문을 사용해 객체에 속성을 검사할 수 있다.

요약


인터페이스와도 동작하기 때문에 인터페이스를 구현하는 객체가 이 타입을 갖는다. 주어진 속성의 존재 여부를 확인하거나 instanceof를 사용하는게 아닌 실제로 존재하는 값을 검사하므로 타입의 안정성을 보장해준다. 또한 해당값의 자동완성을 지원하므로 오타를 방지한다.