4.Node.js/JavaScript&TypeScript

[TypeScript] 4편. Union, Literal, Intersection 타입 이해하기 : typeof, keyof, instanceof

쿼드큐브 2025. 12. 16. 18:01
반응형
반응형

 

4편. Union, Literal, Intersection 타입 이해하기 : typeof, keyof, instanceof

 

📚 목차
1. 유니언 타입(Union Types): 하나 이상의 타입을 허용하는 방식
2. 리터럴 타입(Literal Types): 특정 값만 허용하기
3. 인터섹션 타입(Intersection Type): 여러 타입을 결합하기
4. 타입 연산자(typeof / keyof), instanceof 이해하기

 

타입 이해하기 삽화 이미지
타입 이해하기 삽화 이미지

 

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /TypeScript

 

1. 유니언 타입(Union Types): 하나 이상의 타입을 허용하는 방식

TypeScript에서는 변수나 함수의 매개변수 등이 하나의 타입에만 제한되지 않고, 여러 타입 중 하나라도 허용되도록 만들 수 있습니다.

이런 방식을 유니언 타입(Union Type)이라고 부르며, 타입1 | 타입2 와 같은 형식으로 표현합니다.

 

🔷 유니언 타입 정의와 사용

▸ 유니언 타입은 다음처럼 | 기호(수직선)를 사용하여 선언합니다.

▸ 유니언 타입은 두 개 이상의 타입 중 하나만 일치해도 유효한 값을 허용합니다.
▸ 실무에서는 사용자 입력값, API 응답, ID 등 다양한 케이스에 자주 활용됩니다.

let priceOrText: number | string;

priceOrText = 10000;     // number 할당 가능
priceOrText = "무료";     // string 할당 가능

// priceOrText = true;   // 오류: 'boolean' 타입은 허용되지 않습니다.

 

🔷 타입 좁히기 (Type Narrowing): 유니언 타입 안전하게 다루기

유니언 타입을 사용하면, 코드 작성 시 "이 값이 지금 어떤 타입인지 확실하지 않은 상태"가 됩니다.

이러한 경우, 타입스크립트는 양쪽 타입 모두에 안전한 작업만 허용합니다.


예를 들어, number | string 타입 변수에 .toUpperCase()와 같은 문자열 전용 메서드를 직접 사용하면
TypeScript는 오류를 발생시킵니다. 왜냐하면 이 값이 number일 수도 있기 때문입니다.

이럴 때 사용하는 것이 바로 타입 좁히기(Type Narrowing)입니다.

 

✔️ typeof 연산자를 활용한 타입 좁히기 예시

function printID(id: number | string) {
  // 오류: id가 string이 아닐 수도 있기 때문입니다.
  // console.log(id.toUpperCase()); 

  if (typeof id === "string") {
    // 이 블록 안에서는 id가 string 타입임을 확실하게 알 수 있습니다.
    console.log(`ID는 문자열입니다: ${id.toUpperCase()}`); // 안전하게 사용 가능
  } else {
    // 여기는 id가 number임이 확실합니다.
    console.log(`ID는 숫자입니다: ${id + 10}`); // 숫자 연산 가능
  }
}

printID(12345);        // 출력: ID는 숫자입니다: 12355
printID("abc-xyz");    // 출력: ID는 문자열입니다: ABC-XYZ

▸ typeof 연산자를 사용하여 런타임에 타입을 검사할 수 있습니다.

▸ 검사 결과에 따라 TypeScript는 해당 블록 안에서 해당 타입만 허용하는 것처럼 인식하게 됩니다.

▸ 이를 통해 유니언 타입으로 선언된 변수도 타입 안정성을 유지한 채 안전하게 다룰 수 있습니다.

 

2. 리터럴 타입(Literal Types): 특정 값만 허용하기

TypeScript에서는 특정 값 자체를 타입으로 사용하는 "리터럴 타입"이라는 기능을 제공합니다.

이 기능을 통해 변수나 함수의 인자에 정해진 값만 허용되도록 제한할 수 있어, 코드의 의도와 안정성을 더욱 명확하게 만들 수 있습니다.

 

✔️ 리터럴 타입이란 무엇인가요?

리터럴 타입은 문자열, 숫자, 불리언 값 등 고정된 값 그 자체를 타입으로 사용합니다.
이 방식은 특히 UI 컴포넌트의 상태, API 응답 코드, 방향값 등에서 잘못된 값의 사용을 방지하는 데 매우 유용합니다.

 

🔷 문자열 리터럴 타입

// Status 타입은 오직 아래 세 가지 값만 허용합니다.
type Status = "pending" | "success" | "error";

let currentStatus: Status = "pending"; // 정상
currentStatus = "success";             // 정상

// currentStatus = "finished"; 
// 오류: Type '"finished"' is not assignable to type 'Status'.
function updateStatus(status: Status) {
  console.log(`현재 상태는 ${status}입니다.`);
}

updateStatus("error");    // 출력: 현재 상태는 error입니다.
// updateStatus("ready"); // 오류 발생

리터럴 타입을 함수의 인자 타입으로 사용하면, 허용된 값 외에는 아예 함수 호출 자체가 막히므로 실수로 잘못된 값이 들어오는 것을 미리 차단할 수 있습니다.

 

🔷 숫자 및 불리언 리터럴 타입

문자열뿐 아니라 숫자와 불리언 값도 리터럴 타입으로 사용할 수 있습니다.

type Direction = 1 | -1;

let move: Direction = 1;  // 
move = -1;                // 
// move = 0;              // 오류: Type '0' is not assignable to type 'Direction'.
type AlwaysTrue = true;

let flag: AlwaysTrue = true;  // 
// flag = false;              // 오류: Type 'false' is not assignable to type 'true'.

 

🔷 리터럴 타입 vs enum

항목 리터럴 타입 enum
정의 고정된 값 그 자체를 타입으로 사용 이름 붙인 상수 집합을 만드는 문법
형태 `"left" "right"`
코드 길이 짧고 간단함 조금 더 길고 구조화됨
런타임 코드 ❌ 없음 (타입 전용) ✅ 있음 (JS 객체 생성됨)
실무 추천 ✅ 대부분 이걸 권장 ⚠️ 일반 enum은 비권장 (const enum만 제한적 허용)
언제 쓰나? 단순한 옵션 제한에 적합 enum 스타일의 코드가 필요한 경우에만
//리터럴 타입
type Status = "pending" | "success" | "error";
let s: Status = "pending"; // 

// enum
enum Status {
  Pending = "pending",
  Success = "success",
  Error = "error"
}
let s: Status = Status.Pending; //
반응형

 

3. 인터섹션 타입(Intersection Type): 여러 타입을 결합하기

✔️ 인터섹션 타입이란?

인터섹션 타입이란, 여러 개의 타입을 하나로 합쳐서 모든 타입 조건을 동시에 만족하는 새로운 타입을 만드는 방법입니다.
TypeScript에서는 앰퍼샌드(&) 기호를 사용하여 인터섹션 타입을 정의할 수 있으며, 이는 “A 타입도 만족하고, B 타입도 만족해야 한다”는 의미입니다

 

🔷 인터섹션 타입 정의 및 활용

아래는 두 개의 인터페이스를 인터섹션 타입으로 결합한 예시입니다.

interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

// 'Person'은 HasName과 HasAge를 모두 만족해야 함
type Person = HasName & HasAge;

const myProfile: Person = {
  name: "이아름",  // ✅ HasName의 속성
  age: 25          // ✅ HasAge의 속성
};

 

오류 예시: 속성이 일부만 있는 경우

const partialProfile: Person = {
  name: "김철수"
  // ❌ Error: Property 'age' is missing in type '{ name: string; }'
};

인터섹션 타입은 모든 타입의 속성을 전부 포함해야 합니다. 하나라도 누락되면 TypeScript는 오류로 판단합니다.

 

🔷 유니언 타입 vs 인터섹션 타입

구분 Union 타입 (A | B) Intersection 타입 (A & B)
의미 A 또는 B 중 하나만 만족하면 됨 A와 B 모두 만족해야 함
객체 타입 예시 A의 속성 또는 B의 속성 중 일부만 있어도 됨 A의 모든 속성과 B의 모든 속성이 있어야 함
기본 타입 예시 string | number → 둘 중 하나 허용 string & number → 불가능한 조합
사용 목적 타입의 범위 확장 타입의 정밀한 결합
// 유니언 타입: string 또는 number 중 하나
let value: string | number;

value = "안녕하세요"; // ✅ string
value = 123;         // ✅ number
// value = true;    // ❌ 오류: boolean은 허용되지 않음

// 인터섹션 타입 예시 (불가능한 경우)
type Impossible = string & number;
// const x: Impossible = ???; // ❌ 이 타입은 실질적으로 존재할 수 없음

 

4. 타입 연산자(typeof / keyof), instanceof 이해하기

TypeScript는 기존에 정의된 값(value)이나 타입(type)을 기반으로 새로운 타입을 만들 수 있는 타입 연산자(Type Operators)를 제공합니다.
그 중에서도 가장 기본적이고 강력한 도구인 typeof와 keyof는 이후 배울 제네릭(Generic), 유틸리티 타입, 조건부 타입 등을 이해하는 데 필수적인 개념입니다.

연산자 위치 역할 사용처 예시
typeof 런타임 값의 타입을 문자열로 반환 타입 체크, 타입 좁히기 (JS 공통)
typeof 타입 위치 값에서 타입을 추출 상수/객체/함수 타입 재사용
keyof 타입 위치 (TS) 객체 타입의 키를 유니언 타입으로 추출 안전한 속성 접근, Mapped Type
instanceof 런타임 특정 클래스/생성자의 인스턴스인지 검사 클래스 기반 타입 좁히기

 

🔷 typeof 연산자: 값에서 타입을 추출하기

typeof는 원래 JavaScript의 연산자지만, TypeScript에서는 두 가지 문맥에서 사용됩니다.
▸ 런타임: JavaScript와 동일하게 “값의 타입을 문자열로 반환하는 연산자”
▸ 타입 위치(Type Position): “값으로부터 타입을 추출하는 타입 연산자”
둘의 개념을 분리해서 이해해 봅시다.

 

1. 런타임 typeof (JavaScript 공통)
▸ JavaScript부터 있던 연산자로, 실행 시점에 값의 타입(종류)을 문자열로 반환합니다.
▸ 반환값은 "string", "number", "boolean", "object", "undefined", "function", "symbol", "bigint" 등입니다.

const a = 123;
const b = "hello";
const c = () => {};

console.log(typeof a); // "number"
console.log(typeof b); // "string"
console.log(typeof c); // "function"

 

TypeScript에서도 이 동작은 JavaScript와 동일하며, 이 결과를 이용해 타입 좁히기(type narrowing) 를 할 수 있습니다.

function printValue(v: number | string) {
  if (typeof v === "string") {
    // 여기서는 v가 string으로 좁혀짐
    console.log(v.toUpperCase());
  } else {
    // 여기서는 v가 number로 좁혀짐
    console.log(v.toFixed(2));
  }
}

 

2. 타입 위치에서의 typeof (TypeScript 전용 기능)

▸ TypeScript는 typeof를 타입 선언부에서도 사용할 수 있도록 확장했습니다.

▸ 이때 typeof는 더 이상 문자열 "number"를 반환하는 것이 아니라, “해당 값의 타입을 그대로 가져오는(type query) 역할”을 합니다.

 

1) 객체/변수의 타입 재사용

▸ 기존에 선언된 값의 구조를 그대로 타입으로 가져올 수 있습니다.
▸ 객체 구조가 변경되면 타입도 자동으로 반영되어, 하드코딩을 줄여줍니다.

const user = {
  name: "Alice",
  age: 30,
};

type User = typeof user;
// User 타입은 { name: string; age: number; } 가 됨

const u: User = {
  name: "Bob",
  age: 26,
};

 

2) 함수 타입 추출

▸ 라이브러리 함수, 유틸 함수 등의 시그니처를 그대로 재사용할 때 유용합니다.

function greet(name: string): string {
  return `Hello, ${name}`;
}

type GreetFn = typeof greet;
// => (name: string) => string

const myGreet: GreetFn = (n) => `Hi, ${n}`;

 

3) 상수 객체 + as const + typeof + 인덱싱

▸ 리터럴 타입과 조합하면 “옵션 값”을 깔끔하게 관리하는 패턴으로 자주 쓰입니다.

const STATUS = {
  READY: "READY",
  DONE: "DONE",
  ERROR: "ERROR",
} as const;

// 키 유니언: "READY" | "DONE" | "ERROR"
type StatusKey = keyof typeof STATUS;

// 값 유니언: "READY" | "DONE" | "ERROR"
type StatusValue = (typeof STATUS)[StatusKey];

function setStatus(status: StatusValue) {
  console.log(`상태: ${status}`);
}

setStatus("DONE");   // ✅
// setStatus("PENDING"); // ❌ 컴파일 에러

 

🔷 keyof 연산자: 객체 타입의 키를 유니언으로 추출하기 (TS 전용)

keyof는 객체 타입의 모든 속성 이름(키)을 문자열 리터럴 유니언 타입으로 추출하는 연산자입니다.
이것을 사용하면, 객체 속성에 정확한 키만 접근하도록 타입 제한을 줄 수 있습니다.

interface User {
  id: number;
  name: string;
  email: string;
}

type UserKey = keyof User;
// "id" | "name" | "email"

let k1: UserKey = "id";     // ✅
let k2: UserKey = "email";  // ✅
// let k3: UserKey = "age"; // ❌ 'age'는 UserKey에 없음

이렇게 keyof로 키를 타입으로 뽑아내면, “존재하는 키만 사용할 수 있도록” 제약을 걸 수 있습니다.

 

1. keyof + 제네릭: 안전한 속성 접근 함수

가장 대표적인 활용은 제네릭과 함께 안전한 getter 유틸을 만드는 것입니다.

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
};

const nameVal = getValue(user, "name"); // string 타입으로 추론
// const wrong = getValue(user, "notExist"); // ❌ 컴파일 에러

▸ K extends keyof T 덕분에 key에는 T의 실제 키만 올 수 있습니다.
▸ T[K] 덕분에 반환 타입도 obj[key]의 타입으로 정확히 추론됩니다.

 

2. keyof typeof: 상수 객체에서 키와 값을 동시에 활용

앞서 본 typeof와 결합하면 상수 객체에서 키/값을 타입으로 쉽게 뽑아낼 수 있습니다.

const PERMISSIONS = {
  READ: "read",
  WRITE: "write",
  DELETE: "delete",
} as const;

type PermissionKey = keyof typeof PERMISSIONS;
// "READ" | "WRITE" | "DELETE"

type PermissionValue = (typeof PERMISSIONS)[PermissionKey];
// "read" | "write" | "delete"

let key: PermissionKey = "WRITE";     // ✅
let val: PermissionValue = "delete";  // ✅
// val = "admin"; // ❌ 허용되지 않음

▸ 실제 코드에서 사용하는 상수 객체 하나만 잘 관리하면,
▸ 키/값 타입이 자동으로 따라와서 타입과 값의 불일치를 상당 부분 줄일 수 있습니다.

 

3. keyof + Mapped Types: 전 타입을 한 번에 변형하기

keyof와 Mapped Type을 결합하면, 기존 타입을 변형한 새 타입을 손쉽게 만들 수 있습니다.

type NullableUser = {
  [K in keyof User]: User[K] | null;
};

/*
NullableUser는 다음과 같음:
{
  id: number | null;
  name: string | null;
  email: string | null;
}
*/

▸ 기존 필드명(keyof User)은 그대로 유지하면서,
▸ 각 필드 타입에 | null만 추가하는 전환을 한 번에 수행합니다.

 

🔷 instanceof: 런타임 인스턴스 검사 + 타입 좁히기

instanceof는 JavaScript부터 존재하는 런타임 연산자입니다.
특정 객체가 어떤 생성자/클래스의 인스턴스인지를 프로토타입 체인을 기준으로 검사합니다.

class Person {
  constructor(public name: string) {}
}

const p = new Person("Alice");

console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
console.log(p instanceof Array);  // false

TypeScript에서는 이 결과를 이용해 유니언 타입을 안전하게 좁히는 용도로도 많이 사용합니다.

 

1. instanceof를 사용한 타입 좁히기

class Dog {
  bark() {
    console.log("멍멍!");
  }
}

class Cat {
  meow() {
    console.log("야옹!");
  }
}

type Animal = Dog | Cat;

function speak(animal: Animal) {
  if (animal instanceof Dog) {
    // 여기서는 animal이 Dog 타입으로 좁혀짐
    animal.bark();
  } else {
    // 여기서는 animal이 Cat 타입으로 좁혀짐
    animal.meow();
  }
}

▸ if (animal instanceof Dog) 블록 내부에서는 animal이 Dog 타입으로 인식됩니다.
▸ else 블록에서는 자동으로 Cat 타입으로 좁혀집니다.
▸ 이런 패턴은 클래스 기반 OOP 코드에서 자주 등장합니다.

 

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형