공부해봅시당
[Typescript] 타입 확장하기 & 좁히기 본문
타입 확장하기
앞으로 등장할 집합의 개념은 `속성의 집합`이 아니라 `값의 집합`임
유니온 타입
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 |