Web/TypeScript

[TS] 타입스크립트 이해하기 - 타입 단언, 타입 좁히기, 서로소 유니온 타입

ansui 2024. 11. 17. 00:11

타입 단언

❗타입 단언을 사용하면 타입 체크를 할 수 없다.

타입 선언은 할당되는 값이 해당 인터페이스를 만족하는 검사하는데,

타입 단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 하는 것

타입 단언(as Type)보다는 타입 선언(: Type)을 사용하기

// 타입 단언 (Type assertion)
type Person = {
  name: string;
  age: number;
};

// name, age가 없어서 오류 발생
let person1: Person = {};
person1.name = "안수이";
person1.age = 24;

let person2 = {} as Person;

type Dog = {
  name: string;
  color: string;
};

let dog: Dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도", // 오류 발생
} as Dog; // 오류 해결

// 타입 선언
const alice: Person = { 
	name: 'Alice',
    age: 25,
    }; 

// 타입 단언
const bob = { 
	name: 'Bob',
	age: 20,
} as Person;

 

1️⃣ 타입 단언의 규칙

각 as 단언 ← 단언식 (A as B)
A가 B의 수퍼타입이거나 서브타입이어야 한다.

// 타입 단언의 규칙
let num1 = 10 as never; // 10이 never의 수퍼타입이므로 가능
let num2 = 10 as unknown; // 10이 never의 서브타입이므로 가능
let num3 = 10 as string; // 오류 발생
let num4 = 10 as unknown as string; // 오류 해결이지만 권장하지는 않는다 - 다중 단언

 

2️⃣ const 단언

리터럴로 취급. 즉 const로 선언한 것과 같이 만들어준다.

// const 단언
let num5 = 10 as const;
let cat = {
  name: "애옹",
  color: "yellow",
} as const; // 모든 프로퍼티가 readonly로 추론된다.

// 오류 발생 - 읽기 전용 속성이므로 바꿀 수 없다.
cat.name = "";

 

3️⃣ Non Null 단언

어떤 값이 null이거나 undefined이 아니라고 알려주는 역할

 

Optional Chaining (?.)

?.은 ?.'앞’의 평가 대상이 undefined나 null이면 평가를 멈추고 undefined를 반환한다.

// Non Null 단언
type Post = {
  title: string;
  author?: string; // 익명도 가능하므로 ?를 붙여 선택적 프로퍼티로 설정
};

let post: Post = {
  title: "게시글 1",
  author: "안수이",
};

// author의 길이를 출력하여 len에 저장
// 옵셔널 체이닝: undefined이 될 수 있지만 number에 넣을 수 없기 때문에 오류 발생
const len1: number = post.author?.length;

// !를 넣으면 무조건 null, undefined이 아닐거라고 믿어서 오류가 발생하지 않는다.
// 그냥 그렇게 믿는 것이기 때문에 위험하다 !
const len2: number = post.author!.length;

타입 좁히기

조건문 등을 이용해 넓은 타입에서 좁은타입으로 타입을 상황에 따라 좁히는 방법

// 타입 좁히기
type Person = {
  name: string;
  age: number;
};

/* 
value가 number이면 toFixed()
value가 string이면 toUpperCase()
value가 Date이면 getTime()
value가 Person이면 name은 age살입니다 
*/
function func(value: number | string | Date | null | Person) {
  // 타입에 따라 사용할 수 있는 함수가 다르므로 조건문 바깥에서는 오류 발생
  value.toFixed();
  value.toUpperCase();

  // 변수가 특정 조건문 내부에서 더 좁은 타입임을 보장할 수 있으면 그렇게 추론한다.
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if(typeof value === "object") {
    console.log(value.getTime()); // object는 null도 가능하므로 오류 발생
  } else if (value instanceof Date) {
    // instanceof는 Date객체인지 묻고 맞으면 true 반환
    console.log(value.getTime());
  } else if (value && "age" in value) {
    // instanceof는 우측에 타입이 오면 안된다. 
    // in 뒤에는 null, undefined가 오면 안된다. 따라서 &&를 사용해서 value가 있는 경우에만 사용
    console.log(`${value.name}은 ${value.age}살 입니다.`);
  }
}

서로소 유니온 타입

교집합이 없는 타입들로만 만든 유니온 타입

string 리터럴 타입을 넣어서 서로소 유니온 타입으로 만든다.

 

1️⃣ 예시 1

Admin → {name}님 현재까지 {kickCount}명 강퇴했습니다.
Member {name}님 현재까지 {point}점 모았습니다.
Guest {name}님 현재까지 {visitCount}번 방문하셨습니다.

// 서로소 유니온 타입
type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number; // 강퇴 회원 수
};

type Member = {
  tag: "MEMBER";
  name: string;
  point: number;
};

type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number; // 방문 횟수
};

type User = Admin | Member | Guest;

// 기존 코드 -> 주석이 없으면 누가 어떤 타입인지 알기 어렵다.
function login1(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}번 방문하셨습니다.`);
  }
}

💡예시1) 서로소 유니온으로 해결

// 훨씬 직관적
// string 리터럴로 태그를 사용하면 무조건 타입이 좁혀진다.
function login2(user: User) {
  if (user.tag === "ADMIN") {
    // Admin 타입
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
  } else if (user.tag === "MEMBER") {
    // Member 타입
    console.log(`${user.name}님 현재까지 ${user.point}점 모았습니다.`);
  } else {
    // Guest 타입
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문하셨습니다.`);
  }

  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;
  }
}

 

2️⃣ 예시 2

로딩중 콘솔에 로딩중 출력
실패 실패: 에러메시지를 출력
성공 성공: 데이터를 출력

// 예시 2
// 비동기 작업의 결과를 처리하는 객체
type LoadingTask = {
  state: "LOADING";
};

type FailedTask = {
  state: "FAILED";
  error: {
    message: string;
  };
};

type SuccessTask = {
  state: "SUCCESS";
  response: {
    data: string;
  };
};

// 기존 방법
type AsyncTask = {
  state: "LOADING" | "FAILED" | "SUCCESS";
  error?: {
    message: string;
  };
  response?: {
    data: string;
  };
};

function peocessResult(task: AsyncTask) {
  switch (task.state) {
    case "LOADING":
      console.log("로딩 중");
      break;
    case "FAILED":
      // ?를 지우면 오류 발생 -> 좁혀질 타입이 없다.
      console.log(`에러 발생: ${task.error?.message}`); 
      break;
    case "SUCCESS":
      console.log(`성공: ${task.response?.data}`);
      break;
  }
}

💡예시2) 서로소 유니온으로 해결

// 예시2 해결
type LoadingTask = {
  state: "LOADING";
};

type FailedTask = {
  state: "FAILED";
  error: {
    message: string;
  };
};

type SuccessTask = {
  state: "SUCCESS";
  response: {
    data: string;
  };
};

// 서로소 유니온 타입으로 정의
type AsyncTask = LoadingTask | FailedTask | SuccessTask;

function peocessResult(task: AsyncTask) {
  switch (task.state) {
    case "LOADING":
      console.log("로딩 중");
      break;
    case "FAILED":
      // ?.을 지워도 오류 발생 X
      console.log(`에러 발생: ${task.error?.message}`); 
      break;
    case "SUCCESS":
      console.log(`성공: ${task.response?.data}`);
      break;
  }
}

이정환님의 인프런 강의 "한 입 크기로 잘라 먹는 타입스크립트(TypeScript)"를 참고하여 작성하였습니다.