공부해봅시당

[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