logoRawon_Log
홈블로그소개

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

Typescript

Typescript Comprehansion 2편

Rawon
2025년 11월 24일
목차
🎯 대수 타입
합집합 (Union Type) ➕
교집합 (Intersection Type) ✖️
🎯 타입 추론
타입 추론이 가능한 상황들 ✅
🎯 타입 단언
타입 단언의 규칙 📋
const 단언 🔒
Non Null 단언 ❗
🎯 타입 좁히기
in 타입 가드 🔑
🎯 서로소 유니온 타입
태그 프로퍼티 활용 🏷️
실전 예시: 비동기 작업 상태 관리 ⚙️

목차

🎯 대수 타입
합집합 (Union Type) ➕
교집합 (Intersection Type) ✖️
🎯 타입 추론
타입 추론이 가능한 상황들 ✅
🎯 타입 단언
타입 단언의 규칙 📋
const 단언 🔒
Non Null 단언 ❗
🎯 타입 좁히기
in 타입 가드 🔑
🎯 서로소 유니온 타입
태그 프로퍼티 활용 🏷️
실전 예시: 비동기 작업 상태 관리 ⚙️

이 글은 아래 강의를 바탕으로 정리한 글입니다. 🤗

plain
https://inf.run/UGoRu

🎯 대수 타입

💡 대수 타입이란 여러 개의 타입을 합성해서 새롭게 만들어 낸 타입을 의미합니다.
이러한 대수 타입에는 **합집합 타입(Union 타입)**과 **교집합 타입(Intersection 타입)**이 존재합니다.

합집합 (Union Type) ➕

아래와 같이 | 를 활용하여 string, number의 유니온 타입을 정의할 수 있으며,

아래 예시의 경우에는 변수 a에 number 타입과 string 타입에 해당하는 값이라면 뭐든지 저장할 수 있게 됩니다.

typescript
let a: string | number;
a = "hello";
a = 1;

유니온 타입으로 배열 타입을 정의하려면 아래와 같이 변수 뒤에 (type)[] 과 같은 형태로 타입을 정의해주면 됩니다.

typescript
let arr: (number | string | boolean)[] = [1, "hello", true];

또한, 다음과 같이 여러 개의 객체 타입의 유니온 타입도 정의할 수 있습니다.

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

type Person = {
  name: string;
  language: string;
};

type Union1 = Dog | Person;

(...)

let union1: Union1 = { // ✅
  name: "",
  color: "",
};

let union2: Union1 = { // ✅
  name: "",
  language: "",
};

let union3: Union1 = { // ✅
  name: "",
  color: "",
  language: "",
};

이렇게 정의된 Union1 타입은 교집합이 존재하는 두 집합으로 표현할 수 있습니다.

다만, 다음과 같은 객체는 포함하지 않습니다.

typescript
let union4: Union1 = { // ❌
  name: "",
};

image.png


교집합 (Intersection Type) ✖️

intersection 타입은 다음과 같이 & 를 활용하여 정의할 수 있습니다.

typescript
let variable: number & string;

⚠️ 위의 경우 number 타입과 string 타입은 서로 교집합을 공유하지 않는 서로소 집합이기 때문에 변수 variable의 타입은 결국 never 타입으로 추론됩니다.

대부분의 기본 타입들 간에는 서로 공유하는 교집합이 없기 때문에 이런 intersection 타입은 보통 객체 타입들에 자주 사용됩니다.

두 객체의 타입의 Intersection 타입을 정의해보면 다음과 같습니다.

typescript
type Dog2 = {
  name: string;
  color: string;
};

type Person2 = {
  name: string;
  language: string;
};

type Intersection1 = Dog2 & Person2;

const intersection1: Intersection1 = {
  name: "doldol",
  color: "brown",
  language: "english",
};
typescript
// ❌ 오류 발생 (교집합 타입 중 color 프로퍼티가 없기 때문)
const intersection2: Intersection1 = {
  name: "john",
  language: "english",
};

🎯 타입 추론

🔍 TS는 타입이 정의되어 있지 않은 변수의 타입을 자동으로 추론합니다.
따라서 TS는 프로그래머에게 모든 변수에 일일이 타입을 정의하지 않아도 되는 편리함을 제공합니다.

다만, 모든 상황에 타입을 잘 추론하는 것은 아닙니다.

대표적으로 함수의 매개변수 타입은 자동으로 추론할 수 없습니다.

이렇게 타입 추론이 불가능한 변수(예를 들어 매개변수)에는 암시적으로 any 타입이 추론되나 결국 타입 검사 시 이는 오류로 판단됩니다.

타입 추론이 가능한 상황들 ✅

반대로 타입 추론이 가능한 상황들은 아래와 같습니다.

1. 변수 선언 시

2. 구조 분해 할당

3. 함수의 반환 값

4. 기본값이 설정된 매개변수

typescript
function func(message = "hello") {
  return "hello";
}

5. const 상수의 추론

const 로 선언된 상수도 타입 추론이 진행됩니다. 그러나 let 으로 선언한 변수와는 다른 방식으로 추론되니 주의해야 합니다.

typescript
const num = 10;
// 10 Number Literal 타입으로 추론

const str = "hello";
// "hello" String Literal 타입으로 추론
// 상수는 초기화 때 설정한 값을 변경할 수 없기 때문에 특별히 가장 좁은 타입으로 추론됩니다.

🎯 타입 단언

💪 타입 단언은 TS가 자동으로 타입을 추론하지 못하는 상황에서 개발자가 직접 타입을 지정하는 방법을 말합니다.

예를 들어 아래와 같이 변수 person은 Person 타입으로 정의되었지만 초기화 할 때에는 빈 객체를 할당했다고 가정하겠습니다.

이때, TS는 빈 객체는 Person 타입이 아니므로 이런 경우를 허용하지 않습니다.

이럴 때, 타입 단언을 활용해서 빈 객체를 Person 타입이라고 TS에게 알려주면 문제가 해결됩니다.

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

let person = {};

// ❌ 오류 발생

person.name
 = "John";
person.age = 30;

아래와 같이 값 as 타입 키워드를 사용하고 타입을 명시하면 앞의 값을 TS컴파일러에게 뒤의 타입으로 간주하라고 알려주는 역할을 수행합니다.

typescript
// 이런 경우 타입 단언(type assertion)을 사용합니다.
let person = {} as Person;

person.name
 = "John";
person.age = 30;

이 외에도 타입 단언은 초과 프로퍼티 검사를 피할 때에도 유용하게 사용됩니다.

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

// ❌ 오류 발생, 초과 프로퍼티 검사 발동
// let dog: Dog = {
//   name: "doldol",
//   color: "brown",
//   breed: "시베리안 허스키",
// };

// 정말 어쩔 수 없이 breed 라는 추가 프로퍼티까지 작성해야 하는 경우에도 타입 단언을 사용할 수 있음.
let dog: Dog = {
  name: "doldol",
  color: "brown",
  breed: "시베리안 허스키",
} as Dog;

이처럼 breed 라는 초과 프로퍼티가 존재하지만 이 값을 Dog 타입으로 단언하여 초과 프로퍼티 검사를 피했습니다.

참고로 아래와 같이 다중 단언도 가능합니다.

이러한 다중 단언의 경우 왼쪽에서 오른쪽으로 단언이 이루어지는데, 예시의 경우 number 타입의 값을 unknown 타입으로 단언, unknown 타입의 값을 string 타입으로 단언합니다.

typescript
let num3 = 10 as unknown as string;

타입 단언의 규칙 📋

📌 타입 단언에도 규칙이 있는데 값 as 타입 과 같은 형태의 단언식에서 값이 단언의 슈퍼타입이거나 값이 단언의 서브타입이어야 한다는 겁니다.

typescript
// never 타입은 모든 타입의 서브 타입이기 때문에 10 즉, Number 타입이 never 타입의 슈퍼타입이므로 가능함.
let num1 = 10 as never;

// unknown 타입은 모든 타입의 슈퍼타입이기 때문에 10 즉, Number 타입이 unknown 타입의 서브타입이므로 가능함.
let num2 = 10 as unknown;

// ❌ 오류 발생, number 타입이 string 타입의 서브타입이나 슈퍼타입이 아니기 때문에 불가능함.
// let num3 = 10 as string;

const 단언 🔒

타입 단언 때에만 사용할 수 있는 const 타입이 존재합니다. 이는 변수를 선언했을 때, 마치 const로 선언한 것처럼 타입이 변경됩니다.

typescript
let num4 = 10 as const; // 10 Number Literal 타입으로 단언됨

let cat = {
  name: "nabi",
  color: "white",
} as const;
// 이런 식으로 const 단언을 사용하면 아래와 같이 readonly 프로퍼티가 되며, 임의로 수정/삭제가 불가함.
// let cat: {
//     readonly name: "nabi";
//     readonly color: "white";
// }

Non Null 단언 ❗

Non Null 단언은 값 as 타입 형태를 따르지 않고 값 뒤에 ! 를 붙여줌으로서 이 값이 undefined거나 Null이 아님을 단언합니다.

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

let post: Post = {
  title: "Hello World",
};

const len: number = 
post.author
!.length;

🎯 타입 좁히기

🔬 타입 좁히기는 조건문 등을 이용해서 넓은 타입에서 좁은 타입으로 상황에 따라 타입을 좁혀나가는 방식을 의미합니다.

예를 들어 다음과 같이 number | string 타입의 매개변수를 받는 함수가 있다고 가정했을 때,

함수 내부에서 value가 number이거나 string일 것으로 기대하고 각각의 메서드를 사용하려고 하면 오류가 발생합니다.

만약 각각의 메서드를 사용하고 싶다면 다음과 같이 조건문을 이용해 value의 타입이 특정 타입임을 보장해주어야 합니다.

이때, if (typeof === ...) 처럼 조건문과 함께 사용해 타입을 좁히는 이런 표현들을 타입 가드 라고 부릅니다.

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

// value => number 타입일 경우 toFixed() 메서드 사용
// value => string 타입일 경우 toUpperCase() 메서드 사용
// value => Date 타입일 경우 getTime() 메서드 사용
function func(value: number | string | Date | Person) {
  value;
  //   value.toFixed(); // ❌ 오류 발생
  //   value.toUpperCase(); // ❌ 오류 발생
  //   value.getTime(); // ❌ 오류 발생

  // 타입 가드
  if (typeof value === "number") {
    return value.toFixed();
  } else if (typeof value === "string") {
    return value.toUpperCase();
    // instanceof를 이용하면 내장 클래스 타입이나 직접 만든 클래스 타입을 보장할 수 있는 타입가드를 만들 수 있음.
    // 즉, instanceof 연산자는 우항에 타입이 들어와서는 안됨.
    // 좌항의 값이 우항에 있는 클래스의 인스턴스인지를 판단하는 연산자이기 때문.
  } else if (value instanceof Date) {
    return value.getTime();
    // 직접 만든 타입과 함께 사용하려면 다음과 같이 in 연산자를 이용해야 함.
    // 좌항의 프로퍼티가 우항에 있는지를 확인, 있으면 true, 없으면 false를 반환.
  } else if ("age" in value) {
    return value.age;
  }
}

✨ instanceof 를 이용하면 내장 클래스 또는 직접 만든 클래스의 타입을 보장할 수 있는 타입 가드를 만들 수 있습니다. (직접 만든 타입과는 함께 사용할 수 없음)


in 타입 가드 🔑

제가 직접 만든 타입과 함께 사용하려면 다음과 같이 in 연산자 를 이용해야 합니다.

typescript
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}살 입니다`);
  }
}

🎯 서로소 유니온 타입

🔀 서로소 유니온 타입은 교집합이 없는 타입들 즉, 서로소 관계에 있는 타입들을 모아 만든 유니온 타입을 의미합니다.

typescript
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) {
  // kickCount 프로퍼티는 Admin 타입에만 존재하기 때문에 Admin 타입인 경우에만 실행됨.
  if ("kickCount" in user) {
    console.log(`${
user.name
}님 현재까지 ${user.kickCount}번 강퇴했습니다.`);
    // point 프로퍼티는 Member 타입에만 존재하기 때문에 Member 타입인 경우에만 실행됨.
  } else if ("point" in user) {
    console.log(`${
user.name
}님 현재까지 ${user.point}점을 획득했습니다.`);
    // visitCount 프로퍼티는 Guest 타입에만 존재하기 때문에 Guest 타입인 경우에만 실행됨.
  } else if ("visitCount" in user) {
    console.log(`${
user.name
}님 현재까지 ${user.visitCount}번 방문했습니다.`);
  }
}

⚠️ 그러나 위와 같이 코드를 작성하면 조건식만 보고 어떤 타입으로 좁혀지는지 바로 파악하기가 조금 어려울 수 있습니다.

태그 프로퍼티 활용 🏷️

이런 경우에는 다음과 같이 각 타입에 태그 프로퍼티를 추가 정의해주면 됩니다.

typescript
type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};

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

type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number;
};

(...)

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}번 오셨습니다`);
  }
}

function loginSwitch(user: User) {
  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;
    }
  }
}

실전 예시: 비동기 작업 상태 관리 ⚙️

또다른 예시로 비동기 작업의 결과를 객체로 표현해보면 아래와 같습니다.

typescript
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;
    }
    default: {
      console.log("알 수 없는 상태입니다.");
      break;
    }
  }
}

const loading: AsyncTask = {
  state: "loading",
};

const failed: AsyncTask = {
  state: "failed",
  error: {
    message: "문제가 발생했습니다.",
  },
};

const success: AsyncTask = {
  state: "success",
  response: {
    data: "데이터입니다.",
  },
};