💡 타입 단언
• 타입 단언이란
// 타입 단언
type Person = {
name: string;
age: number;
};
let person = {}; // ❌
변수 person은 Person 타입으로 정의되었지만 초기화할 때에는 빈 객체를 넣어두고 싶을 경우가 있을 수 있습니다. 그러나 타입스크립트에서는 다음과 같은 경우를 허용하지 않습니다. 빈 객체는 Person 타입이 아니므로 오류가 발생하게 됩니다.
let person = {} as Person;
person.name = "수현";
person.age = 27;
이럴 때에는 다음과 같이 빈 객체를 Person 타입이라고 타입스크립트에게 단언해주면 됩니다. 값 as 타입 으로 특정값을 원하는 타입으로 단언하는 것을 타입 단언이라고 합니다.
type Dog = {
name: string;
color: string;
};
let dog = {
name: "별이",
color: "brown",
breed: "시츄", // 초과프로퍼티
} as Dog;
타입 단언은 다음과 같이 초과 프로퍼티 검사를 피할 때에도 유용합니다. 변수 dog에 Dog 타입을 지정해주고 싶지만 breed 라는 추가 프로퍼티가 존재하면 as 타입으로 초과 프로퍼티 검사를 피할 수 있습니다.
• 타입 단언의 조건
값 as 타입 형식의 단언식을 A as B 로 표현하였을 때 아래의 두가지 조건 중 하나를 반드시 충족해야 합니다.
⓵ A가 B의 슈퍼타입이다.
⓶ A가 B의 서브타입이다.
let num1 = 10 as never;
let num2 = 10 as unknown;
let num3 = 10 as string; // ❌
num1 ) A(number 타입)의 값을 B(never) 타입으로 단언합니다. never 타입은 모든 타입의 서브타입 → A가 B의 슈퍼타입
num2 ) A(number 타입)의 값을 B(unknown) 타입으로 단언합니다. unknown 타입은 모든 타입의 슈퍼타입 → A가 B의 서브타입
num3 ) A(number 타입)의 값을 B(string) 타입으로 단언합니다. number와 string 타입은 슈퍼-서브관계 아님 → 단언 불가능 🙅🏻♀️
• 다중 단언
let num3 = 10 as unknown as string;
타입 단언은 다중으로도 가능합니다. 다중 단언을 사용하면 앞서 살펴본 예제 중 불가능했던 단언이 가능해집니다. 이러한 다중 단언의 경우 이러한 과정을 거져 왼쪽에서 오른쪽으로 단언이 이루어 집니다.
⓵ number 타입의 값을 unknown 타입으로 단언합니다.
⓶ unknown 타입의 값을 string 타입으로 단언합니다.
이렇게 중간에 값을 unknown 타입으로 단언하면 unknown 타입은 모든 타입의 슈퍼타입이므로 모든 타입으로 다시 또 단언하는게 가능해집니다. 이는 슈퍼-서브 관계를 가지지 않는 타입에서 사용하는데 정말 어쩔 수 없이 불가피한 상황에서만 사용하기를 권장합니다.
• const 단언
let num4 = 10 as const; // 10 Number Literal 타입으로 단언됨
let cat = {
name: "깜둥이",
color: "black",
} as const; // 모든 프로퍼티가 readonly 상태가 되도록 단언됨
cat.name = "나비"; // ❌
특정 값을 const 타입으로 단언하면 마치 변수를 const로 선언한 것과 비슷하게 타입이 변경됩니다. 마지막 줄 코드를 보면 cat이라는 객체의 name 프로퍼티 값을 변경하고자 하였는데 cat이라는 객체는 현재 const로 선언된 것과 같기 때문에 변경이 불가능하여 에러가 발생한 것입니다.
• Non Null 단언
type Post = {
title: string;
author?: string;
};
let post: Post = {
title: "게시글1",
author: "수현", //선택프로퍼티(?)기 때문에 없어도 됨
};
const len: number = post.author!.length;
Non Null 단언은 지금까지 살펴본 값 as 타입 형태를 따르지 않는 단언입니다. 값 뒤에 느낌표(!)를 붙여주면 이 값이 undefined이거나 null이 아닐 것으로 단언할 수 있습니다.
💡 타입 좁히기
function func(value: number | string) {
value.toUpperCase(); // ❌
value.toFixed(); // ❌
}
다음과 같은 함수가 있다고 가정하겠습니다. 이때 매개변수 value의 타입이 number | string 이므로 함수 내부에서도 value가 number 타입이거나 string 타입일 것으로 생각하고 메서드를 사용하려고 하면 오류가 발생합니다.
• type of 타입가드
function func(value: number | string) {
if (typeof value === "number") {
console.log(value.toFixed());
} else if (typeof value === "string") {
console.log(value.toUpperCase());
}
}
만약 value가 number 타입일 것이라고 생각하고 number 메서드인 toFixed를 사용하고 싶으면 다음과 같이 조건문을 이용해 value의 타입이 number 타입임을 보장해주어야합니다. value가 string 타입일 것이라고 생각하고 string 메서드인 toUpperCase를 사용하고 싶을 때도 동일하게 타입을 명시해주면 됩니다. 이렇게 조건문을 이용하여 조건문 내부에서 변수가 특정 타입임을 보장하면 해당 조건문 내부에서는 변수의 타입이 보장된 타입으로 좁혀집니다. 따라서, 첫번째 조건문 내부에서는 value의 타입이 number 타입이 되고, 두번째 조건문 내부에서는 value의 타입이 string 타입이 됩니다. 이를 타입 좁히기 라고 표현합니다.
• instanceof 타입가드
function func(value: number | string | Date | null) {
if (typeof value === "number") {
console.log(value.toFixed());
} else if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (value instanceof Date) { //instanceof 타입카드 사용
console.log(value.getTime());
}
}
instanceof를 이용하면 내장 클래스 타입을 보장할 수 있는 타입가드를 만들 수 있습니다. 하지만 instanceof는 내장 클래스 또는 직접 만든 클래스에만 사용이 가능한 연산입니다. 따라서 우리가 직접 만든 타입과 함께 사용할 수 없습니다.
• in 타입가드
type Person = {
name: string;
age: number;
};
function func(value: number | string | Date | null | Person) {
if (typeof value === "number") {
console.log(value.toFixed());
} else if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (value instanceof Date) {
console.log(value.getTime());
} else if (value && "age" in value) {
console.log(`${value.name}은 ${value.age}살 입니다`)
}
}
만약 우리가 직접 만든 타입과 함께 사용하고자 하면 다음과 같이 in 연산자를 이용해야 합니다. age 라는 프로퍼티값이 없으면 해당 콘솔은 발생하지 않기 때문에 value가 참일 때 라는 조건을 같이 추가해서 나열해주었습니다.
💡 서로소 유니온 타입
서로소 유니온 타입은 교집합이 없는 타입들 즉 서로소 관계에 있는 타입들만 모아 만든 유니온 타입을 의미합니다.
type Admin = {
name: string;
kickCount: number;
};
type Member = {
name: string;
point: number;
};
type Guest = {
name: string;
visitCount: number;
};
type User = Admin | Member | Guest;
function login(user: User) {
if ("kickCount" in user) {
// Admin
console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다`);
} else if ("point" in user) {
// Member
console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다`);
} else {
// Guest
console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다`);
}
}
다음과 같은 간단한 회원 관리 프로그램이 있다고 가정하였습니다. 역할 분류에 따라 3개의 타입을 각각 정의해 주었고, 3개 타입의 합집합 타입인 User 타입도 만들어 주었습니다. login 이라는 함수는 User 타입의 매개변수 user를 받아 회원의 역할에 따라 각각 다른 기능을 수행하도록 합니다.
⓵ 첫번째 조건문이 참이면, user에 kickCount 프로퍼티가 존재함 → Admin 타입입니다.
⓶ 두번째 조건문이 참이면, user에 point 프로퍼티가 존재함 → Member 타입입니다.
⓷ 세번째 else문까지 오면, user는 자동으로 남는 타입으로 좁혀짐 → Guest 타입입니다.
그러나, 이렇게 코드를 작성하게 되면 어떤 타입인지 바로 파악하기 어렵기 때문에 직관적인 코드가 아닌 것 같습니다.
// 각각에 tag 프로퍼티를 추가함
type Admin = {
tag: "ADMIN";
name: string;
kickCount: number;
};
type Member = {
tag: "MEMBER";
name: string;
point: number;
};
type Guest = {
tag: "GUEST";
name: string;
visitCount: number;
};
이러한 경우에는 다음과 같이 각 타입에 태그 프로퍼티를 추가 정의해주면 됩니다.
⓵ Admin 타입에는 "ADMIN" String Literal 타입의 tag 프로퍼티 추가 정의
⓶ Member 타입에는 "MEMBER" String Literal 타입의 tag 프로퍼티 추가 정의
⓷ Guest 타입에는 "GUEST" String Literal 타입의 tag 프로퍼티 추가 정의
function login(user: User) {
if (user.tag === "ADMIN") {
console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다`);
} else if (user.tag === "MEMBER") {
console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다`);
} else {
console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다`);
}
}
그러면 이제 login 함수의 타입 가드를 다음과 같이 더 직관적으로 수정할 수 있게 되었습니다.
function login(user: User) {
switch (user.tag) {
case "ADMIN": {
console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
break;
}
case "MEMBER": {
console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다.`);
break;
}
case "GUEST": {
console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
break;
}
}
}
또는 switch 구문을 이용하여 더 직관적으로 코드를 변경할 수도 있습니다.
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 함수 오버로딩과 커스텀 타입가드로 복잡한 타입 관리하기 (0) | 2024.12.19 |
---|---|
[ TypeScript ] 함수 타입: 정의와 표현식, 호출 시그니처, 타입 호환성 (0) | 2024.12.19 |
[ TypeScript ] 대수 타입 & 타입 추론: 강력한 타입 시스템 이해하기 (0) | 2024.12.19 |
[ TypeScript ] 타입 계층도와 함께 살펴보는 타입 호환성 (0) | 2024.12.19 |
[ TypeScript ] any, unknown, void, never : 헷갈리는 타입 한눈에 보기 (0) | 2024.12.18 |