Typescript - 10

hoin_lee·2023년 8월 2일
0

TypeScript

목록 보기
11/14

타입 좁히기 (Narrowing)

typeof 가드

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

Truthiness가드를 사용해서 간단하고 명확하게 null,undefined,falsy 값을 좁히거나 제거할 수 있다.

const el = document.getElementById("idk") // HtmlElement | null
if(el){
  el // truthy를 통과했기에 HtmlElement 타입으로 확인
}else {
  el // falsy이기 때문에 null타입으로 확인
}

Equality

바로 예시부터 확인해보면 이해하기 쉽다

function someDemo(x: string | number, y: string | boolean){
  if(x===y){
    x // string타입
    y // string타입
  }
}

xstring타입을 가지고 있고 ystring타입을 가지고 있기때문에 둘다 같으려면 ===를 사용했고 타입도 동일하게 된다

In

일단 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

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 타입으로 인식
}
}

타입 명제(Predicates)

이건 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을 사용한다 한들 타입좁히기는 할 수 없다
그냥 단지 booleantruefalse를 반환하는 함수이기 때문이다

그래서 isCat의 반환 타입을 적는 부분에 animal is Cat이라는 반환할 때 true일 경우 animalCat타입이다 라고 명시해줌으로써 타입 좁히기가 완성된다

판별유니온

타입 판별에 도움이 되는 패턴인데 타입 좁히기에 도움이 된다.
핵심 개념은 공통적인 프로퍼티를 공유하는 여러 유형을 생성하고 해당 프로퍼티가 리터럴 타입 즉 리터럴 값이 되는 것이다

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연산자를 사용했었는데 해당 프로퍼티가 있는지 확인해서 타입을 좁힐 수 있었다.
다만, 이런 패턴의 안 좋은 점은 타입별 inout을 알아야 하고 공통점과 차이점은 무엇인지 알아야 한다는 점이다

Pig | Rooster | Cow모두 같은 속성을 가지고 있어 getFarmAnimalSound 함수에서 사용되는 파라미터인 animal이 대체 어떤 타입인지 확인할 수가 없다.
이 경우 유니온을 판별할 각각의 인터페이스 kind라는 공통 프로퍼티를 가지게 한다.
여기서 무조건 kind여야 할 필요는 없다 TYPE이라고도 하고 __type이라고도 쓰듯 자유롭지만 컨벤션에 맞춰 작성하고 다만 일관성은 꼭 지키자

이름이 어렵긴 한데 그냥 이름표를 붙이는 작업과같다

소진 검사(Exhaustiveness check)오 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"
}

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 타입까지 내려갔다면 오류가 발생하게 된다!
그게 어떤 동물이든 animalnever에 할당되지 않으니까 말이다

위의 예시 그대로에서 양이라는 동물을 추가해보자

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타입을 할당할 수 없다고 한다. 즉, 실수했다고 알려주는 장치이다!( 처리하지 않은 케이스가 있다는 뜻 )

profile
https://mo-i-programmers.tistory.com/

0개의 댓글