[TypeScript] 섹션4. 타입스크립트 이해하기(3)

jaehoon ahn·2025년 2월 5일

TypeScript

목록 보기
6/14
post-thumbnail

타입 단언

type Person = {
  name: string;
  age: number;
};

let person:Person = {};
person.name  "제노";
person.age = 27;

빈 객체로 초기화 하고 타입을 지정해버리면, person에는 name, age가 없어서 오류가 발생
나중에 name, age를 초기화 해주고 타입 정의를 해서 타입 정의를 지우면, 초기화에서 오류가 발생
→ 이는 빈 객체를 기준으로 추론하기 때문에 빈 객체가 되어 오류가 발생한다.

초기화 값의 타입을 단언해주기

let person = {} as Person;
person.name = "제노";
person.age = 27;

⇒ 위와 같이 as 연산자를 써주면 된다.

또 다른 예시

type Dog = {
  name: string;
  color: string;
};
let dog: Dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도",
};

// 초과 프로퍼티가 발동하여 breed에서 오류 발생
// 근데 breed를 꼭 써야 하는 경우

초과 프로퍼티가 발동하여 breed에서 오류가 발생
근데 꼭 breed를 써야하는 경우에는 다음과 같이 as를 사용해주면 된다.

let dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도",
} as Dog;

타입 단언의 규칙

  1. 값 as 단언 ← 단언식
  2. A as B
    1. A가 B의 슈퍼타입이거나
    2. A가 B의 서브 타입이어야 한다.

A가 B의 슈퍼타입인 경우

let num1 = 10 as never;

A가 B의 서브타입인 경우

let num2 = 10 as unknown;

슈퍼타입도, 서브타입도 아닌 경우

let num3 = 10 as string;
// A는 number, B는 string타입, 서로 겹치는 값이 없어서 교집합이 없는 경우
// 즉, 서로가 슈퍼타입, 서브타입이 아니라서 오류가 발생한다.

const 단언

예시1

let num4 = 10 as const;
// number literal type 10으로 추론이 된다.

예시2

let cat = {
  name: "야옹이",
  color: "yellow",
} as const;
// 모든 프로퍼티가 읽기 전용으로 된 객체로 추론된다.

cat.name = "";
// 즉, 이렇게 프로퍼티 값을 수정할 수 없는 객체가 된다.
// 따라서, 프로퍼티에 모두 readonly를 사용하지않고 const 단언을 하면 된다.

Non Null 단언 (!)

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

type Post = {
  title: string;
  author: string;
};

let post: Post = {
  title: "게시글1",
  author: "제노",
};
const len: number = post.author?.length;
// ? : js에서 제공하는 옵셔널 체이닝
// null이거나 undefined일 경우 값 자체를 undefined가 나오게 하는 것
// 값 자체가 undefined가 될 수 있어서 number 타입으로 정의한 값에 undefined가 들어갈 수 없기 떄문에 오류가 발생
const len: number = post.author!.length;
// !를 쓰면 오류 해결
// !를 사용하면 이 값이 null이거나 undefined가 아닐 것이라고 
// ts 컴파일러가 믿도록 만드는 것

타입 좁히기

⇒ 조건문 등을 이용해 넓은 타입에서 좁은 타입으로 좁히는 것

⇒ 타입을 상황에 따라 좁히는 방법을 이야기한다.

예시1(number, string)

// value => number: toFixed 적용
// value => string: toUpperCase 적용

function func(value: number | string) {
  value;
  value.toUpperCase();
  value.toFixed();
  //   number | string이기 때문에 오류 발생

  if (typeof value === "number") {
    console.log(value.toFixed());
    // 이 조건문 내에서는 number 타입임이 보장됌
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
    // 이 조건문 내에서는 string 타입임이 보장됌
  }
//   타입을 좁힐 수 있는 표현을 ts에서는 type guard라고 부른다.
}

예시2(Date, null 타입 추가)

// value => number: toFixed 적용
// value => string: toUpperCase 적용
// value => Date: getTime
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 (typeof value === "object") {
    console.log(value.getTime());
  }
  //   typeof는 null값에 적용해도 object를 똑같이 반환해서 null이 있으면 오류가 발생함
}

예시2 오류 해결

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) {
    console.log(value.getTime());
  }
  //   instanceof 사용
  //  왼쪽에 있는 값이 오른쪽의 인스턴스냐 라고 묻는 문법
  // value값의 타입이 Date객체일 것이 보장되기 때문에 Date 객체로 추론되며 오류가 해결된다.
}

예시3


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 instanceof Person) {
    // 오류 발생
    // instanceof는 우측에 있는 항에 타입이 들어오면 안된다.
    // 클래스가 아니기 때문에, instanceof 연산자 뒤에 사용할 수 없다.
    // in 연산자 사용
  }
}

예시3 오류 해결


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 연산자와 && 연산자를 사용해줘서 해결해주면 된다.

서로소 유니온 타입

⇒ 교집합이 없는 타입들로만 만든 유니온 타입을 말함.
ex) number 타입과 string 타입이 있다. ⇒ number | string
⇒ 이를 서로소 집합이라고 부른다.

언제 쓸모가 있을지?

⇒ 코드가 직관적이지 않을 경우에 사용된다.

type Admin = {
  name: string;
  kickCount: number;
};
type Member = {
  name: string;
  point: number;
};
type Guest = {
  name: string;
  visitCount: number;
};
type User = Admin | Member | Guest;

// Admin -> {name}님 현재까지 {kickCount}명 강퇴했습니다.
// Member -> {name}님 현재까지 {point} 모았습니다.
// Guest -> {name}님 현재까지 {visitCount}번 오셨습니다.
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}번 방문하셨습니다.`);
  }
  //  이렇게 코드를 작성하면 조건문만 보고 어떤 타입을 취급하는지 직관적으로 알 수 없다.
}
// 이럴때 서로소 유니온 타입을 사용한다.

사용법

type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};
type Member = {
  tag: "MEMBER";
  name: string;
  point: number;
};
type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number;
};

위와 같이 모든 타입에 tag 프로퍼티를 추가해서 admin, member, guest를 분류할 수 있도록 해준다.

전체 코드

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 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}번 방문하셨습니다.`);
  }
}
// 훨씬 직관적으로 코드를 짤 수 있다.
// 객체 타입에 각각 string literal로 정의된 프로퍼티드을 다르게 정의해주면 
// 서로소 유니온 타입으로 만들 수 있기 때문에, 직관적으로 타입을 좁혀서 처리할 수 있다.

추가 예시

type AsyncTask = {
  state: "LOADING" | "FAILED" | "SUCCESS";
  error?: {
    message: string;
  };
  response?: {
    data: string;
  };
};

// 로딩중 -> 콘솔에 로딩중 출력
// 실패 -> 실패 : 에러메시지를 출력
// 성공 -> 성공 : 데이터를 출력
function processResult(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;
    }
  }
}
// 좁혀질 타입이 없으므로 옵셔널 체이닝을 써주거나 !를 써줘야 한다.
// 안전한 코드가 아니므로, AsynTask를 3개의 타입으로 서로소 유니온 타입으로 
// 만들어주면 된다.

코드

type LoadingTask = {
  state: "LOADING";
};
type FailedTask = {
  state: "FAILED";
  error: {
    message: string;
  };
};
type SuccessTask = {
  state: "SUCCESS";
  response: {
    data: string;
  };
};

이렇게 타입을 3개로 분리해주고, state를 통해 분류할 수 있도록 한다.

전체 코드

type LoadingTask = {
  state: "LOADING";
};
type FailedTask = {
  state: "FAILED";
  error: {
    message: string;
  };
};
type SuccessTask = {
  state: "SUCCESS";
  response: {
    data: string;
  };
};

type AsyncTask = LoadingTask | FailedTask | SuccessTask;
function processResult(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;
    }
  }
}

0개의 댓글