function triple(value: number | string){
// hihihi 아니면 숫자에 *3을 return하는 함수
}
이 상황에서 이전에 배운대로라면 if
문을 통해 타입 검사를 확인하고 해당 타입에 맞는 코드 식을 쓰는 것처럼 해결했다 그때 사용한 typeof
가 있었는데
typeof
는 문자열, 숫자, 불리언과 같은 원시 값을 처리할 때 유용하다
문자열, 숫자 다 가능하지만 다른 부분도 있다. typeof [2,3]
이나 typeof {title:"belog"}
나 둘 다 'object'
라고 출력된다 무적이 아니니 조심하자.
(이전 클린코드 다룰 때 한번 나왔던 얘기이다)
function triple(value: number | string){
if(typeof value === "string") {
return value.repeat(3)
}
return value * 3
}
이것이 원시형 타입에 대입되는 typeof 가드이다.
Truthiness가드를 사용해서 간단하고 명확하게 null
,undefined
,falsy
값을 좁히거나 제거할 수 있다.
const el = document.getElementById("idk") // HtmlElement | null
if(el){
el // truthy를 통과했기에 HtmlElement 타입으로 확인
}else {
el // falsy이기 때문에 null타입으로 확인
}
바로 예시부터 확인해보면 이해하기 쉽다
function someDemo(x: string | number, y: string | boolean){
if(x===y){
x // string타입
y // string타입
}
}
x
도 string
타입을 가지고 있고 y
도 string
타입을 가지고 있기때문에 둘다 같으려면 ===
를 사용했고 타입도 동일하게 된다
일단 in 연산자는 명시된 속성이 명시된 객체 안에 존재한다는 걸로 boolean
값을 반환하는데
예시부터 확인해보자
interface Movie {
title: string;
duration: number;
}
interface TVShow{
title: string;
numEpisodes: number;
episodeDuration: number;
}
function getRuntime(media: Movie | TVShow){
if("numEpisodes" in media){
return media.numEpisodes * media.episodeDuration //TVShow 타입
}
return media.duration
}
instanceof는 연산자로 주어진 값의 프로토타입 체인 내에 해당 프로토타입 체인 내에 존재하는지 확인하는 연산자이다
특정 클래스의 인스턴스에 값이 존재하는지 등을 확인할 수 있다
function printFullDate(date: string | Date){
if(date instanceof Date){
console.log(date.toUTCString()); // Date타입으로 인식
} else {
console.log(new Date(date).toUTCString());
}
}
date를 두 가지 형시으로 받는 이상한 함수 같지만 비교적 흔한 작업이라고 한다. Date
객체로도 작업을 하지만 대부분 date 문자열로도 작업 할 때가 있기 때문
그리고 instanceof는 클래스에서도 사용가능한데
class User {
constructor(public username:string){}
}
class Company{
constructor(public name: string){}
}
function printName(entity: User | Company) {
if(entity instanceof User) {
entity // User 타입으로 인식한다
} else {
entity // Company 타입으로 인식
}
}
이건 typescript에만 있는 기능으로 타입 명제의 근본적인 기능은 함수를 정의하는 것이다
그러면 이 함수는 typescript에게 OO의 타입이 무엇인지를 알려주게 된다
interface Cat {
name: string;
numLives: number;
}
interface Dog {
name: string;
breed: string;
}
function isCat(animal: Cat | Dog): animal is Cat{
return (animal as Cat).numLives !== undefined
}
function makeNoise(animal: Cat | Dog): string {
if(isCat(animal)){
animal // Cat 타입으로 인식함
return "Meow"
} else {
animal // Dog 타입으로 인식
}
}
isCat
이라는 함수를 만들어 고양이인지 확인하도록 했지만 만약 반환 타입으로 적어놓은 animal is Cat
을 적지 않는다면 아래 makeNoise
함수에서 isCat
을 사용한다 한들 타입좁히기는 할 수 없다
그냥 단지 boolean
인 true
와 false
를 반환하는 함수이기 때문이다
그래서 isCat
의 반환 타입을 적는 부분에 animal is Cat
이라는 반환할 때 true
일 경우 animal
은 Cat
타입이다 라고 명시해줌으로써 타입 좁히기가 완성된다
타입 판별에 도움이 되는 패턴인데 타입 좁히기에 도움이 된다.
핵심 개념은 공통적인 프로퍼티를 공유하는 여러 유형을 생성하고 해당 프로퍼티가 리터럴 타입 즉 리터럴 값이 되는 것이다
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"
}
type FarmAnimal = Pig | Rooster | Cow;
function getFarmAnimalSound(animal: FarmAnimal){
switch(animal.kind){
case("pig"):
return "Oink!";
case("cow"):
return "Mooooo!";
case("rooster"):
return "Cockadoodle!"
}
}
const stevie: Rooster = {
name: "Chicks",
weight; 2,
age: 1.5,
kind: "rooster"
}
getFarmAnimalSound(stevie)
이전에는 in
연산자를 사용했었는데 해당 프로퍼티가 있는지 확인해서 타입을 좁힐 수 있었다.
다만, 이런 패턴의 안 좋은 점은 타입별 in
과 out
을 알아야 하고 공통점과 차이점은 무엇인지 알아야 한다는 점이다
Pig | Rooster | Cow
모두 같은 속성을 가지고 있어 getFarmAnimalSound
함수에서 사용되는 파라미터인 animal
이 대체 어떤 타입인지 확인할 수가 없다.
이 경우 유니온을 판별할 각각의 인터페이스 kind
라는 공통 프로퍼티를 가지게 한다.
여기서 무조건 kind
여야 할 필요는 없다 TYPE
이라고도 하고 __type
이라고도 쓰듯 자유롭지만 컨벤션에 맞춰 작성하고 다만 일관성은 꼭 지키자
이름이 어렵긴 한데 그냥 이름표를 붙이는 작업과같다
위에서 사용한 예시를 그대로 가져와서 사용해보자
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"
}
type FarmAnimal = Pig | Rooster | Cow;
function getFarmAnimalSound(animal: FarmAnimal){
switch(animal.kind){
case("pig"):
return "Oink!";
case("cow"):
return "Mooooo!";
case("rooster"):
return "Cockadoodle!"
default:
//이곳까지는 도착하지 않도록 해야한다
const _exhaustiveCheck: never = animal
}
}
만약 우리가 변경된 내용을 처리하는 걸 잊어버리면 코드에서 이를 알려줘야 가장 좋은 방법이다
위의 예시에서 사용된 never
타입은 이전에도 다뤘는데 어디든 할당되지만 never
에는 어떤 타입도 할당할 수 없다
그래서 switch..case
문에서 default
타입까지 내려갔다면 오류가 발생하게 된다!
그게 어떤 동물이든 animal
은 never
에 할당되지 않으니까 말이다
위의 예시 그대로에서 양이라는 동물을 추가해보자
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"
}
type FarmAnimal = Pig | Rooster | Cow | Sheep;
function getFarmAnimalSound(animal: FarmAnimal){
switch(animal.kind){
case("pig"):
return "Oink!";
case("cow"):
return "Mooooo!";
case("rooster"):
return "Cockadoodle!"
default:
//이곳까지는 도착하지 않도록 해야한다
const _exhaustiveCheck: never = animal //에러가발생
return _exhaustiveCheck
}
}
이 경우 정상적으로 interface Sheep
을 만들었고 FarmAnimal
이라는 타입별칭에 추가도 했다.
하지만 getFarmAnimalSound
함수의 switch..case
문에 추가하는 것을 깜빡했더니 typescript는 case문을 전부 내려가 defalut문까지 도착하게 되고 default에서 존재하는 _exhaustiveCheck
이란 변수는 never
타입이기때문에 Sheep
타입을 할당할 수 없다고 한다. 즉, 실수했다고 알려주는 장치이다!( 처리하지 않은 케이스가 있다는 뜻 )