타입 좁히기(Narrowing)란 주로 명확하지 않은 타입이 있을 때 사용합니다.
typeof는 문자열, 숫자, 불리언(boolean)과 같은 원시 값을 처리할 때 유용합니다.
참조 타입의 경우 배열을 array가 아닌 object라고만 표시되어 사용하기 힘듭니다.
Typeof Guards는 조건문에 typeof를 사용하는 것을 말합니다.
만약 조건문에서 return이 사용되었다면 TypeScript에서는 코드를 분석해 다음 value는 숫자임을 인식하게됩니다.
function triple(value: number | string) {
if (typeof value === "string") {
// string 이라면 반환되었다고 TypeScript가 판단
return value.repeat(3);
}
// (parameter) value: number
return value * 3;
}
Truthiness Guards를 사용하면 null, undefined, falsy 값을 좁히거나 제거할 수 있습니다.
document.getElementById에서 타입을 지정하지 않는다면 HTMLElement|null 둘 중 하나가 될 수 있습니다.
조건문으로 간단한 Truthy 체크를 하여 결과를 불리언 타입으로 볼 수 있습니다.
만약 undefined나 null 또는 어떤 다른 falsy 값이 주어지면 if문의 코드는 실행되지 않고 else문의 코드가 실행됩니다.
// el: HTMLElement | null
const el = document.getElementById("idk");
if (el) {
// el: HTMLElement
el;
} else {
// el: null
el;
}
선택적 프로퍼티 사용하여 사용되지 않아도 오류가 발생하지 않는 매개변수 word가 있습니다.
조건문을 통해 사용되었다면 if문의 코드를, 안되었다면 else문의 코드를 사용되게 할 수 있습니다.
else문에서 word의 타입을 보면 string | undefined라고 되어 있는데,
문자열을 사용한 Truthiness Guards의 경우 빈 문자열은 문자열이지만 falsy에 해당합니다.
따라서 TypeScript이 문자열이 아니라고 확신하지 못했기 때문에 타입이 좁혀지지 않았습니다.
// (parameter) word: string | undefined
const printLetters = (word?: string) => {
if (word) {
// (parameter) word: string
for (let char of word) {
console.log(char);
}
} else {
// (parameter) word: string | undefined
word
console.log("YOU DID NOT PASS IN A WORD");
}
};
비교 연산자(=)를 사용한 타입 좁히기입니다.
x === y가 되는 경우는 x의 타입과 y의 타입이 string인 경우밖에 없습니다.
function someDemo(x: string | number, y: string | boolean) {
if (x === y) {
// (parameter) x: string
x.toUpperCase();
}
}
in 연산자를 사용한 타입 좁히기입니다.
조건문에 in연산자를 사용하여 객체 안에 특정 속성이 있다면 코드를 사용하는 방법입니다.
numEpisodes는 TVShow interface에만 있기 때문에 조건문에서는 TVShow 타입의 조건문에서 객체를 사용하는 코드를 반환하고,
TVShow 타입의 객체라면 이미 조건문에서 반환되었기 때문에 이후 코드는 Movie타입으로 좁혀집니다.
interface Movie {
title: string;
duration: number;
}
interface TVShow {
title: string;
numEpisodes: number;
episodeDuration: number;
}
function getRuntime(media: Movie | TVShow) {
if ("numEpisodes" in media) {
// (parameter) media: TVShow
return media.numEpisodes * media.episodeDuration;
}
// (parameter) media: Movie
return media.duration;
}
console.log(getRuntime({ title: "mother", duration: 128 }));
console.log(
getRuntime({ title: "Bargain", numEpisodes: 6, episodeDuration: 35 })
);
instanceof는 연산자로 특정 클래스의 인스턴스에 값이 존재하는지 등을 확인하여 타입을 좁히는 방식입니다.
Date에서 인스턴스화되었는지 또는 일반 문자열에서 인스턴스화되었는지를 확인하여 타입을 좁힙니다.
function printFullDate(date: string | Date) {
if (date instanceof Date) {
// (parameter) date: Date
console.log(date.toUTCString());
} else {
// (parameter) date: string
console.log(new Date(date).toUTCString());
}
}
printFullDate(new Date())
printFullDate('December 25, 2023 23:59:59')
아래와 같은 경우 모두 객체이기 때문에 Typeof Guards를 사용할 수 없습니다.
그리고 모두 name을 가지고 있기 때문에 in Operator Narrowing도 사용할 수 없습니다.
instanceof Narrowing을 사용하면 User 클래스에서 혹은 Company 클래스에서 인스턴스화되었는지 구분하여 범위를 좁힐 수 있습니다.
class User {
constructor(public name: string) {}
}
class Company {
constructor(public name: string) {}
}
function printName(entity: User | Company) {
if (entity instanceof User) {
// (parameter) entity: User
entity;
} else {
// (parameter) entity: Company
entity;
}
}
Type Predicates은 매개변수 is 타입 구문을 말합니다.
반환값이 boolean인 함수를 정의하고 반환 타입을 매개변수 is 타입(Type Predicates)으로 합니다.
만약 반환 값이 true라면 매개변수가 매개변수 is 타입에 작성된 타입이라는 것을 TypeScript에게 알려줍니다.
만약 Type Predicates을 사용한 함수에서 true가 반환된다면,
animal is Cat에서 TypeScript는 animal이 Cat임을 알게됩니다.
interface Cat {
name: string;
numLives: number;
}
interface Dog {
name: string;
breed: string;
}
// Type Predicates을 사용한 함수
function isCat(animal: Cat | Dog): animal is Cat {
// animal이 Cat 타입임을 단언
// 정말 animal이 Cat 타입이여서 numLives를 가지고 있다면 true를 반환
return (animal as Cat).numLives !== undefined;
}
function makeNoise(animal: Cat | Dog): string {
// (parameter) animal: Cat | Dog
if (isCat(animal)) {
// (parameter) animal: Cat
animal;
return "Meow";
} else {
// (parameter) animal: Dog
animal;
return "Woof!";
}
}
Type Predicates를 사용하지 않아도 동일하게 작동합니다.
하지만 이 구문을 사용함으로써 TypeScript가 이 함수가 전달된 값이 Cat인지 아닌지를 판단한다는 걸 알게 할 수 있다는 차이점이 있습니다.
그리고 Type Predicates을 사용한 타입 좁히기는 타입을 좁힐 수 있는 재사용이 가능한 함수가 생겼다는 게 중요합니다.
interface Cat {
name: string;
numLives: number;
}
interface Dog {
name: string;
breed: string;
}
function isCat(animal: Cat | Dog) {
return (animal as Cat).numLives !== undefined;
}
function makeNoise(animal: Cat | Dog): string {
// (parameter) animal: Cat | Dog
if (isCat(animal)) {
// (parameter) animal: Cat | Dog
animal;
return "Meow";
} else {
// (parameter) animal: Cat | Dog
animal;
return "Woof!";
}
}
공통적인 프로퍼티를 공유하는 여러 유형을 생성하고 해당 프로퍼티가 리터럴 타입이자 리터럴 값이 됩니다.
이를 통해 조건문인 switch 문을 쓸 수 있고 TypeScript는 공통적인 프로퍼티로 타입을 찾거나 좁힐 수 있습니다.
exhaustiveness checking은 가능한 모든 옵션을 다 썼는지 확인하기 위해 사용됩니다.
만약 case에 없는 kind를 가진 매개변수를 받는다면 case에서 처리되지 못하게 되고
default로 가게됩니다.
default에서 _exhaustiveCheck변수에 초기화 되는데 never에는 어떤 타입도 할당할 수 없기 때문에 오류가 발생하게 됩니다.
(Type 'case에 처리되지 못한'타입 is not assignable to type 'never'.)
// interface들이 kind라는 공통적인 프로퍼티를 갖음
interface Rooster {
name: string;
weight: number;
age: number;
kind: "rooster";
}
interface Cow {
name: string;
weight: number;
age: number;
kind: "cow";
}
interface Pig {
name: string;
weight: number;
age: number;
kind: "pig";
}
interface Sheep {
name: string;
weight: number;
age: number;
kind: "sheep";
}
// interface Rooster | interface Cow | interface Pig | interface Sheep
type FarmAnimal = Rooster | Cow | Pig | Sheep;
function getFarmAnimalSound(animal: FarmAnimal) {
// (parameter) animal: FarmAnimal
// (property) kind: "rooster" | "cow" | "pig" | "sheep"
switch (animal.kind) {
case "pig":
return "Oink!";
case "cow":
return "Moooo!";
case "rooster":
return "Cockadoodledoo!";
case "sheep":
return "Baaa!";
default:
// 모든 case가 올바르게 처리되면 여기까지 진행되지 않음
const _exhaustiveCheck: never = animal;
return _exhaustiveCheck;
}
}
const stevie: Rooster = {
name: "Stevie Chicks",
weight: 2,
age: 1.5,
kind: "rooster",
};
console.log(getFarmAnimalSound(stevie));
// "Cockadoodledoo!"