공부해봅시당

[Typescript] 타입 확장하기 & 좁히기 본문

STUDY/Typescript

[Typescript] 타입 확장하기 & 좁히기

tngus 2024. 7. 30. 01:32

타입 확장하기

앞으로 등장할 집합의 개념은 `속성의 집합`이 아니라 `값의 집합`임

 

유니온 타입

MyUnion은 A와 B의 합집합이다.

A의 값을 가질 수도 있고, B의 값을 가질 수도 있다.

대신 A와 B의 값을 합쳐서 가질 수는 없다.

type A = {
    type: 'A';
    propertyA: string;
};

type B = {
    type: 'B';
    propertyB: number;
};

type MyUnion = A | B;

const value1: MyUnion = {
    type: 'A',
    propertyA: 'Hello'
};

const value2: MyUnion = {
    type: 'A',
    propertyA: 'World'
};

const value3: MyUnion = {
    type: 'B',
    propertyB: 123
};

const value4: MyUnion = {
    type: 'B',
    propertyB: 456
};

const value5: MyUnion = {
    type: 'A',
    propertyB: 123 // Error: 개체 리터럴은 알려진 속성만 지정할 수 있지만 'A' 형식에 'propertyB'이(가) 없습니다.
}

 

교차 타입

MyIntersection은 A와 B의 교집합이다.

type A = {
    propertyA: string;
};

type B = {
    propertyB: number;
};

type MyIntersection = A & B;

const value: MyIntersection = {
    propertyA: 'Hello',
    propertyB: 123
};

 

주의: 속성명이 같을거면 값도 완전히 같아야 교집합

교집합이어야하기 때문에 type이라는 속성명이 A와 B에 동시에 있는데, 값이 다르면 교집합으로 묶을 수 없고 혼란이 생긴다.

type A = {
    type: 'A';
    propertyA: string;
};

type B = {
    type: 'B';
    propertyB: number;
};

type MyIntersection = A & B; // never 타입이 됨

const value: MyIntersection = {
    type: 'A', // Error: 'string' 형식은 'never' 형식에 할당할 수 없습니다.
    propertyA: 'Hello', // Error: 'string' 형식은 'never' 형식에 할당할 수 없습니다.
    propertyB: 123 // Error: 'number' 형식은 'never' 형식에 할당할 수 없습니다.
};

 

아래는 commonType이라는 속성명과 값 A가 완전히 일치하기 때문에 가능하다.

type A = {
    commonType: 'A';
    propertyA: string;
};

type B = {
    commonType: 'A';
    propertyB: number;
};

type MyIntersection = A & B;

const value: MyIntersection = {
    commonType: 'A',
    propertyA: 'Hello',
    propertyB: 123
};

 

 

유니온 타입과 교차 타입 함께 사용해보기

Universal은 number 타입이 된다.

IdType과 Numeric의 공통 부분이 number이기 때문이다.

type IdType = string | number; // string이나 number
type Numeric = number | boolean; // number나 boolean

type Universal = IdType & Numeric; // number 타입이 됨

 

extends와 교차 타입

교차 타입을 쓰는 대신 interface와 extends 키워드를 사용할 수도 있다.

교차 타입은 type 키워드로만 선언할 수 있다.

extends는 interface, class에서만 사용 가능하다.

 

교차 타입 예시

type A = {
    propertyA: string;
};

type B = {
    propertyB: number;
};

type MyIntersection = A & B;

const value: MyIntersection = {
    propertyA: 'Hello',
    propertyB: 123
};

 

interface와 extends 예시

interface A {
    propertyA: string;
}

interface B {
    propertyB: number;
}

interface MyIntersection extends A, B {}

const value: MyIntersection = {
    propertyA: 'Hello',
    propertyB: 123
};

 

extends와 교차 타입 차이

tip이 호환되지 않기 때문에 interface에서는 에러가 난다.

interface Tip {
	tip: number;
}

// Error
//'Filter' 인터페이스가 'Tip' 인터페이스를 잘못 확장합니다.
//  'tip' 속성의 형식이 호환되지 않습니다.
//    'string' 형식은 'number' 형식에 할당할 수 없습니다.
interface Filter extends Tip {
	tip: string;
}

 

하지만 교차 타입에서는 타입이 never로 바뀐다.

type Tip = {
	tip: number;
};

type Filter = Tip & {
	tip: string;
};

 

따라서 interface를 사용하는 것이 확장면에 있어서도 더 좋을 듯 하다.

 

타입 좁히기 - 타입 가드

타입스크립트에서 타입 좁히기는 변수 또는 표현식이 타입 범위를 더 작은 범위로 좁혀나가는 과정을 말한다.

타입 좁히기를 통해 더 정확하고 명시적인 타입 추론을 할 수 있게 되고, 복잡한 타입을 작은 범위로 축소하여 타입 안정성을 높일 수 있다.

 

타입 가드에 따라 분기 처리하기

타입스크립트로 개발하다 보면 여러 타입을 할당할 수 있는 스코프에서 특정 타입을 조건으로 만들어 분기 처리하고 싶을 때가 있다.

유니온 타입이나 any 타입 같이 여러 타입을 받을 수 있는 것은 조건으로 검사하려는 타입보다 넓은 범위를 가지고 있기 때문이다.

타입스크립트에서 분기처리
조건문과 타입 가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에 따라 다른 동작을 수행하는 것

타입 가드
런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능

타입스크립트에서 스코프
변수와 함수 등의 식별자가 유효한 범위
즉, 변수와 함수를 선언하거나 사용할 수 있는 영역

 

런타임에서도 유효하도록 자바스크립트 연산자인 typeof, instanceof, in을 활용한다.

 

원시 타입을 추론할 때: typeof 연산자

// typeof A === B

function checkType(value: any): string {
    if (typeof value === 'string') {
        return 'The value is a string';
    } else if (typeof value === 'number') {
        return 'The value is a number';
    } else if (typeof value === 'boolean') {
        return 'The value is a boolean';
    } else {
        return 'The value is of an unknown type';
    }
}

// 테스트
console.log(checkType('Hello')); // The value is a string
console.log(checkType(123));     // The value is a number
console.log(checkType(true));    // The value is a boolean
console.log(checkType({}));      // The value is of an unknown type

 

typeof는 자바스크립트 타입 시스템만 대응할 수 있다.

자바스크립트의 동작 방식으로 인해 null과 배열 타입 등이 object 타입으로 판별되는 등 복잡한 타입을 검증하기에는 한계가 있다.

따라서 typeof 연산자는 주로 원시 타입을 좁히는 용도로만 사용할 것을 권장한다.

 

typeof 연산자를 사용하여 검사할 수 있는 타입 목록
- string
- number
- boolean
- undefined
- object
- function
- bigint
- symbol

 

인스턴스화된 객체 타입을 판별할 때: instanceof 연산자 활용하기

// A instanceof B

class Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    makeSound() {
        console.log(`${this.name} makes a sound.`);
    }
}

class Dog extends Animal {
    breed: string;

    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }

    makeSound() {
        console.log(`${this.name} barks.`);
    }
}

class Cat extends Animal {
    color: string;

    constructor(name: string, color: string) {
        super(name);
        this.color = color;
    }

    makeSound() {
        console.log(`${this.name} meows.`);
    }
}

function checkInstance(obj: any) {
    if (obj instanceof Dog) {
        console.log(`${obj.name} is a Dog of breed ${obj.breed}.`);
    } else if (obj instanceof Cat) {
        console.log(`${obj.name} is a Cat of color ${obj.color}.`);
    } else if (obj instanceof Animal) {
        console.log(`${obj.name} is an Animal.`);
    } else {
        console.log(`Unknown object.`);
    }
}

// 테스트
const myDog = new Dog('Buddy', 'Golden Retriever');
const myCat = new Cat('Whiskers', 'Tabby');
const myAnimal = new Animal('Generic Animal');
const myString = 'I am not an animal';

checkInstance(myDog);    // Buddy is a Dog of breed Golden Retriever.
checkInstance(myCat);    // Whiskers is a Cat of color Tabby.
checkInstance(myAnimal); // Generic Animal is an Animal.
checkInstance(myString); // Unknown object.

 

객체의 속성이 있는지 없는지에 따른 구분: in 연산자 활용하기

in 연산자는 객체에 속성이 있는지 확인한 다음에 true 또는 false를 반환한다.

in 연산자를 사용하면 속성이 있는지 없는지에 따라 객체 타입을 구분할 수 있다.

class Car {
    make: string;
    model: string;

    constructor(make: string, model: string) {
        this.make = make;
        this.model = model;
    }
}

class Bicycle {
    brand: string;
    gears: number;

    constructor(brand: string, gears: number) {
        this.brand = brand;
        this.gears = gears;
    }
}

function checkProperty(obj: any) {
    if ('make' in obj) {
        console.log(`The object is a Car made by ${obj.make}.`);
    } else if ('brand' in obj) {
        console.log(`The object is a Bicycle of brand ${obj.brand}.`);
    } else {
        console.log(`Unknown object.`);
    }
}

// 테스트
const myCar = new Car('Toyota', 'Corolla');
const myBicycle = new Bicycle('Trek', 21);
const myUnknownObject = { color: 'red' };

checkProperty(myCar);         // The object is a Car made by Toyota.
checkProperty(myBicycle);     // The object is a Bicycle of brand Trek.
checkProperty(myUnknownObject); // Unknown object.

 

 

is 연산자로 사용자 정의 타입 가드 만들어 활용하기

직접 타입 가드 함수를 만들 수도 있다.

이러한 방식의 타입 가드는 반환 타입이 타입 명제인 함수를 정의하여 사용할 수 있다.

타입 명제
함수의 반환 타입에 대한 타입 가드를 수행하기 위해 사용되는 특별한 형태의 함수

 

반환값에 boolean을 사용한 것과 is를 사용한 것의 차이를 보면 이해가 쉽다.

 

is 연산자로 return

// A is B
// A는 매개변수 이름, B는 타입

interface Dog {
    bark: () => void;
}

interface Cat {
    meow: () => void;
}

function isDog(animal: any): animal is Dog {
    return (animal as Dog).bark !== undefined;
}

function makeSound(animal: Dog | Cat) {
    if (isDog(animal)) {
        animal.bark(); // 타입스크립트는 이 시점에서 animal이 Dog 타입임을 알 수 있음
    } else {
        animal.meow(); // 이 경우 animal이 Cat 타입임을 알 수 있음
    }
}

// 테스트
const myDog: Dog = {
    bark: () => console.log('Woof!'),
};

const myCat: Cat = {
    meow: () => console.log('Meow!'),
};

makeSound(myDog); // Woof!
makeSound(myCat); // Meow!

 

Boolean으로 return

interface Dog {
    bark: () => void;
}

interface Cat {
    meow: () => void;
}

function isDog(animal: any): boolean {
    return (animal as Dog).bark !== undefined;
}

function makeSound(animal: Dog | Cat) {
    if (isDog(animal)) {
        animal.bark(); // Error: Property 'bark' does not exist on type 'Dog | Cat'
                      // TypeScript는 이 시점에서 animal이 Dog 타입임을 알 수 없음
    } else {
        animal.meow(); // Error: Property 'meow' does not exist on type 'Dog | Cat'
                      // TypeScript는 이 시점에서 animal이 Cat 타입임을 알 수 없음
    }
}

// 테스트
const myDog: Dog = {
    bark: () => console.log('Woof!'),
};

const myCat: Cat = {
    meow: () => console.log('Meow!'),
};

makeSound(myDog); // 컴파일 오류
makeSound(myCat); // 컴파일 오류

 

타입 좁히기 - 식별할 수 있는 유니온(Discriminated Unions)

타입스크립트에서 유니온 타입을 다룰 때 사용되는 기법이다.

각 타입을 식별할 수 있는 공통 속성(주로 tag나 type 속성)을 추가하여 유니온 타입을 쉽게 구분할 수 있게 한다.

이 기법을 사용하면 타입스크립트가 타입을 더 정확하게 추론할 수 있어 안전한 코드를 작성할 수 있다.

 

아래에서는 type을 공통 속성으로 주었다.

interface NetworkError {
    type: 'NetworkError';
    message: string;
    statusCode: number;
}

interface ValidationError {
    type: 'ValidationError';
    message: string;
    field: string;
}

interface DatabaseError {
    type: 'DatabaseError';
    message: string;
    query: string;
}

type AppError = NetworkError | ValidationError | DatabaseError;

function handleError(error: AppError) {
    switch (error.type) {
        case 'NetworkError':
            console.log(`Network Error: ${error.message}, Status Code: ${error.statusCode}`);
            break;
        case 'ValidationError':
            console.log(`Validation Error: ${error.message}, Field: ${error.field}`);
            break;
        case 'DatabaseError':
            console.log(`Database Error: ${error.message}, Query: ${error.query}`);
            break;
    }
}

// 테스트
const networkError: NetworkError = {
    type: 'NetworkError',
    message: 'Failed to fetch data',
    statusCode: 500,
};

const validationError: ValidationError = {
    type: 'ValidationError',
    message: 'Invalid email address',
    field: 'email',
};

const databaseError: DatabaseError = {
    type: 'DatabaseError',
    message: 'Query failed',
    query: 'SELECT * FROM users',
};

handleError(networkError); // Network Error: Failed to fetch data, Status Code: 500
handleError(validationError); // Validation Error: Invalid email address, Field: email
handleError(databaseError); // Database Error: Query failed, Query: SELECT * FROM users


식별할 수 있는 유니온의 판별자를 선정하는 기준

유닛 타입으로 선언되어야 한다.

유닛 타입
다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입
null, undefined, 리터럴 타입을 비롯해 true, 1 등 정확한 값을 나타내는 타입이 유닛 타입에 해당함
반면, 다양한 타입을 할당할 수 있는 void, string, number와 같은 타입은 유닛 타입으로 적용되지 않음

 

깃허브에서는 아래와 같이 정의한다고 한다.

- 리터럴 타입이어야 한다.

- 판별자로 선정한 값에 적어도 하나 이상의 유닛 타입이 포함되어야 하며, 인스턴스화할 수 있는 타입은 포함되지 않아야 한다.

 

Exhaustiveness Checking으로 정확한 타입 분기 유지하기

Exhaustiveness는 사전적으로 철저함, 완전함을 의미한다.

따라서 Exhaustiveness Checking은 모든 케이스에 대해 철저하게 타입을 검사하는 것을 말하며 타입 좁히기에 사용되는 패러다임 중 하나다.

Exhaustiveness Checking을 사용하면 예상치 못한 런타임 에러를 방지하거나 요구사항이 변경되었을 때 생길 수 있는 위험성을 줄일 수 있다.

따라서 타입에 대한 철저한 분기 처리가 필요하다면 Exhaustiveness Checking 패턴을 활용하는 것이 좋다.

interface Car {
    type: 'car';
    drive: () => void;
}

interface Bike {
    type: 'bike';
    pedal: () => void;
}

interface Plane {
    type: 'plane';
    fly: () => void;
}

// Boat가 새롭게 추가된 상황
interface Boat {
    type: 'boat';
    sail: () => void;
}

type Vehicle = Car | Bike | Plane | Boat;

function operateVehicle(vehicle: Vehicle) {
    if (vehicle.type === 'car') {
        vehicle.drive();
    } else if (vehicle.type === 'bike') {
        vehicle.pedal();
    } else if (vehicle.type === 'plane') {
        vehicle.fly();
    } else {
    	// Error: 'Boat' 형식은 'never' 형식에 할당할 수 없습니다.
        // 이를 통해 새롭게 추가된 Boat 또한 분기 처리를 해줘야 함을 알 수 있음
        const exhaustiveCheck: never = vehicle; 
        throw new Error(`Unhandled vehicle type: ${exhaustiveCheck}`);
    }
}

const myCar: Car = {
    type: 'car',
    drive: () => console.log('Driving a car!'),
};

const myBike: Bike = {
    type: 'bike',
    pedal: () => console.log('Pedaling a bike!'),
};

const myPlane: Plane = {
    type: 'plane',
    fly: () => console.log('Flying a plane!'),
};

const myBoat: Boat = {
    type: 'boat',
    sail: () => console.log('Sailing a boat!'),
};

operateVehicle(myCar); // Driving a car!
operateVehicle(myBike); // Pedaling a bike!
operateVehicle(myPlane); // Flying a plane!
operateVehicle(myBoat); // Error

 


출처

타입스크립트 with 리액트

https://product.kyobobook.co.kr/detail/S000210716282

 

우아한 타입스크립트 with 리액트 | 우아한형제들 - 교보문고

우아한 타입스크립트 with 리액트 | 주니어 프론트엔드 개발자를 위한 타입스크립트+리액트 온보딩 가이드 우아한형제들은 자바스크립트와 자체 개발 웹 프레임워크인 WoowahanJS를 사용했었다. 그

product.kyobobook.co.kr

 

 

'STUDY > Typescript' 카테고리의 다른 글

[Typescript] 타입스크립트 컴파일  (0) 2024.08.20
[Typescript] 타입 활용하기  (0) 2024.08.06
[Typescript] 값과 타입  (1) 2024.07.24
[Typescript] 타입스크립트 타입 선언 & 종류  (24) 2024.07.23
[Typescript] 타입  (1) 2024.07.20