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 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > JavaScript&TypeScript' 카테고리의 다른 글
| [TypeScript] 6편. 제네릭 (Generic) 이해하기: 함수, 인터페이스, 클래스, constraints 활용 (0) | 2025.12.22 |
|---|---|
| [TypeScript] 5편. 클래스에 타입 적용하기: 클래스 정의와 인터페이스 구현 (0) | 2025.12.18 |
| [TypeScript] 3편. 배열, 튜플, 함수 타입 이해하기 : 매개변수, 반환 타입, 오버로딩 (0) | 2025.12.15 |
| [TypeScript] 2편. 객체 설계의 핵심, Interface vs Type Alias 완벽 정리와 타입 추론 원리 (0) | 2025.12.12 |
| [TypeScript] 1편. TypeScript 핵심 개념과 기본 타입 이해하기 (0) | 2025.12.10 |