본문 바로가기
TYPESCRIPT

고급 타입

by 일태찡 2023. 3. 24.

 

인터섹션 타입

 

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

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

// 여기
type ElevatedEmlpoyee = Admin & Employee;

const e1: ElevatedEmlpoyee = {
    name: 'KIM',
    privileges: ['create-server'],
    startDate: new Date()
}

 

interface Admin  {
    name: string;
    privileges: string[];
};

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


interface ElevatedEmlpoyee extends Admin, Employee {}

const e1: ElevatedEmlpoyee = {
    name: 'KIM',
    privileges: ['create-server'],
    startDate: new Date()
}

 

인터섹션 타입은 인터페이스 상속과 밀접한 관련이 있습니다. 즉, 두 코드는 정확히 같은 기능을 합니다.

객체 타입은 객체 속성의 조합을 나타냅니다.

 

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

type Universal = Combinable & Numeric;

 

숫자형 타입이 유일한 인터섹션 타입이기 때문에  Universal은 숫자형 타입으로 간주합니다.

유니언 타입은 타입 간에 공통점이 있는 타입을 나타냅니다.

 

 

인터섹션 연산자는 어떤 타입과도 사용할 수 있어서 이렇게 타입들이 교차하도록 간단하게 구현할 수 있습니다.

 

 

타입가드

 

typeof 타입가드

 

type Combinable = string | number;

function add(a: Combinable, b: Combinable) {
    // 이 조건문을 타입가드라고 함
    if (typeof a === 'string' || typeof b === 'string') {
        return a.toString() + b.toString();
    }
    return a + b;
}

 

위 코드와 같이 정확히 무슨 작업을 수행하는지 타입에 따라 달라지게 할 수 있습니다.

타입가드는 유니온 타입이 지닌 유연성을 활용할 수 있게 해 주며 런타임시 코드가 정확하게 작동하게 해 줍니다.

객체에 관한 타입가드는 typeof를 쓸 수 없고 in과 instanceof 타입가드를 쓸 수 있습니다.

 

 

in 타입가드

 

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('Privileges: ' + emp.privileges); // 에러
}

 

위 코드에선 UnknownEmployee에 두 객체 모두 들어올 수 있기 때문에 name 속성은 둘 다 가지고 있기에 에러가 아니지만 privileges 속성은 Admin만 가지고 있기에 타입가드가 필요합니다.

사용자 정의 객체 타입에는 typeof를 사용할 수 없기에 in 키워드를 이용하여 해결할 수 있습니다.

 

function printEmployeeInformation(emp: UnknownEmployee) {
    console.log('Name: ' + emp.name);
    if('privileges' in emp) {
        console.log('Privileges: ' + emp.privileges);
    }
    if('startDate' in emp) {
        console.log('Start Date: ' + emp.startDate);
    }
}

 

 

instanceof 타입가드

 

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 (vehicle instanceof Truck) {
        vehicle.loadCargo(1000);
    }
}

 

in 타입가드를 사용할 때 문자열에서의 오류 검증이 힘들 수 있기에 더 견고한 방법인 instaceof 사용도 있습니다.

instanceof는 순수 자바스크립트 기능이기에 인터페이스에서 사용할 수 없습니다.

따라서 사용하게 되면 객체가 클래스에 기반하는지 확인할 수 있고 클래스에 기반한다면 그 메서드를 지니고 있는지 확인할 수 있습니다.

 

 

구별된 유니언

 

interface Bird {
	// 여기
    type: 'bird';
    flyingSpeed: number;
}

interface Horse {
	// 여기
    type: 'horse';
    runningSpeed: number;
}

type Animal = Bird | Horse;

function moveAnimal(animal:Animal) {
    let speed;
    switch (animal.type) {
        case 'bird':
            speed = animal.flyingSpeed;
            break;
        case 'horse':
            speed = animal.runningSpeed;
    }
    console.log('Moving with speed: ' + speed);
}

 

위 코드는 인터페이스를 통해 생성된 객체이기 때문에 타입가드를 instanceof로 할 수 없으며 in을 사용할 순 있지만 경우의 수가 늘어날 경우 복잡하고 실수하기 쉽습니다.

그때 이 방법도 활용할 수 있는데 인터페이스마다 공통되는 속성을 적용하고(여기서는 type속성) switch문을 통해 케이스를 나눴습니다.

이 경우에는 in 타입가드와 다르게 케이스에 자동 완성을 표시해 주기 때문에 실수를 줄일 수 있습니다.

 

 

형 변환(typecasting)

 

index.html

...
<body>
  <input type="text" id="user-input">
</body>
...

 

app.ts

const userInputELement = document.getElementById('user-input');

 

위와 같이 코드를 작성해도 userInputElement는 html에 있는 저 인풋 태그를 감지할 능력이 없습니다.

이 문제를 형 변환으로 타입스크립트가 직접 감지하지 못하는 특정 타입의 값을 타입스크립트에 알려줘 해결할 수 있습니다.

 

하나는 변환하고자 하는 요소 앞이나 타입스크립트에 타입을 알려주고자 하는 위치 앞에 무언가를 추가하는 방법

두 개의 방법이 있습니다.

 

const userInputELement = <HTMLInputElement>document.getElementById('user-input')!;

 

하나는 이렇게 변환하고자 하는 요소 앞에 <HTMLInputElement>를 추가하여 형 변환합니다.

 

const userInputELement = document.getElementById('user-input')! as HTMLInputElement;

 

리액트 JSX 구문과의 충돌을 막기 위해 만든 대안이며 선택한 부분 다음에 as 키워드와 어떤 타입으로 형 변환할지 입력하면 됩니다.

 

선택 요소 마지막에 들어간 느낌표는 앞의 표현식을 null로 반환하지 않겠다고 타입스크립트에게 인식시키는 역할을 합니다. 이 느낌표에 대안으로는 다음과 같이 작성할 수 있습니다.

 

const userInputELement = document.getElementById('user-input');

if(userInputELement) {
    (userInputELement as HTMLInputElement).value = 'Hi there!';
}

 

 

인덱스 타입

 

객체가 지닐 수 있는 속성에 대해 보다 유연한 객체를 생성할 수 있게 해주는 기능입니다.

 

interface ErrorContainer {
    id: number; // 에러
    [prop: string]: string; // 인덱스 타입
}

 

정확한 속성 이름을 모르고 속성의 개수도 모르며 이 인터페이스 기반의 객체에 추가되는 모든 속성은 문자열로 해석할 수 있는  속성 이름을 지녀야 한다는 것과 해당 속성에 대한 값 역시 문자열 이어야 한다는 것만 알고 있다고 입력한 것입니다.

그래서 id 속성에 number를 할당할 경우 다음과 같은 에러가 발생합니다.

 

 

interface ErrorContainer {
    [prop: string]: string;
}

const errorBag: ErrorContainer = {
    email: 'Not a valid email!',
    username: 'Must start with a capital character!'
}

 

 

함수 오버로드

 

동일한 함수에 대해 여러 함수 시그니처를 정의할 수 있는 기능입니다.

간단히 말해 다양한 매개변수를 지닌 함수를 호출하는 여러 가지 가능한 방법을 사용하여 함수 내에서 작업을 수행할 수 있게 해 줍니다.

 

type Combinable = number | string;

function add(a: Combinable, b: Combinable) {
    if (typeof a === 'string' || typeof b === 'string') {
        return a.toString() + b.toString();
    }
    return a + b;
}

const result = add('Kim', 'iltae');
result.split('') // 에러

 

위 코드에서 우리는 result가 문자열이라는 걸 알고 있지만 사실 타입스크립트는 알지 못합니다.

 

 

그래서 문자열 메서드인 split을 사용하면 다음과 같은 에러가 나옵니다.

 

 

const result = add('Kim', 'iltae') as string;

 

물론 다음과 같이 형 변환을 하면 해결할  수 있지만 코드를 더 사용해야 하고 최선은 아니기에 함수 오버로드를 사용하는 것도 좋습니다.

 

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
    if (typeof a === 'string' || typeof b === 'string') {
        return a.toString() + b.toString();
    }
    return a + b;
}

const result = add('Kim', 'iltae');
result.split('')

 

이렇게 작성한 함수 위에 오버로드를 작성해 타입스크립트가 자체적으로 반환 타입을 정확히 추론하지 못하는 경우에 사용할 수 있습니다.

함수에서 지원할 수 있는 다양한 조합에 대해 어떤 것이 반환되는지 명확하게 할 수 있습니다.

 

 

선택적 체이닝

 

객체의 일부 속성이 설정되어 있는지 또는 정의되지 않았는지 확실히 알 수 없는 중첩된 데이터로 구조화 작업을 수행하는 경우 사용할 수 있습니다.

 

const fetchedUserData = {
    id: 'a1',
    name: 'KIM',
    job: { title: 'CEO', description: 'My own company' }
};

//                         여기  여기
console.log(fetchedUserData?.job?.title);

 

선택적 체이닝 연산자는 객체 데이터의 중첩된 속성과 객체에 안전하게 접근할 수 있게 해 줍니다.

물음표 앞의 요소가 정의되지 않았다면 그 이후에는 해당 요소에 접근하지 않습니다.

이는 데이터의 존재 여부를 확인하는 if 문으로 컴파일됩니다.

 

 

Null 병합

 

const userInput = null;

//                           여기
const storedData = userInput ?? 'DEFAULT';

 

userInput이 null이거나 undefined라면 'DEFAULT'를 할당하겠다는 것을 의미합니다.

이는 아래 코드와 다르게 userInput이 falsy 한 값 즉 0 또는 빈 문자열('')도 같이 처리하는 것이 아닌 오직 null과 undefined일 경우만 처리합니다.

 

const userInput = null;

const storedData = userInput || 'DEFAULT';

 

'TYPESCRIPT' 카테고리의 다른 글

데코레이터  (8) 2023.03.28
제네릭  (6) 2023.03.27
인터페이스  (7) 2023.03.23
클래스 2  (6) 2023.03.21
클래스 1  (6) 2023.03.20