TypeScript는 JavaScript에 정적 타입을 추가한 언어입니다. 컴파일 시점에 오류를 발견하고, 더 나은 개발 경험을 제공합니다. 이 글에서는 TypeScript의 타입 시스템을 체계적으로 다루겠습니다.
TypeScript 타입 시스템
TypeScript는 다양한 타입을 제공하여 코드의 안정성을 높입니다.
기본 타입
let name: string = "TypeScript";
let count: number = 42;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
let uniqueKey: symbol = Symbol("key");
let bigNumber: bigint = 9007199254740991n;
배열과 튜플
let numbers: number[] = [1, 2, 3, 4, 5];
let strings: Array<string> = ["a", "b", "c"];
let point: [number, number] = [10, 20];
let record: [string, number, boolean] = ["age", 25, true];
const rgb: readonly [number, number, number] = [255, 128, 0];
객체 타입
let user: { name: string; age: number } = {
name: "Kim",
age: 30
};
interface User {
id: number;
name: string;
email: string;
age?: number;
readonly createdAt: Date;
}
type Product = {
id: string;
name: string;
price: number;
};
interface Admin extends User {
role: string;
permissions: string[];
}
유니온과 인터섹션 타입
유니온 타입
type StringOrNumber = string | number;
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
type Status = "pending" | "approved" | "rejected";
function setStatus(status: Status) {
console.log(`Status: ${status}`);
}
인터섹션 타입
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const person: Person = {
name: "Kim",
age: 30
};
type WithTimestamp<T> = T & {
createdAt: Date;
updatedAt: Date;
};
type UserWithTimestamp = WithTimestamp<User>;
유니온은 “A 또는 B” (A | B), 인터섹션은 “A와 B 모두” (A & B)입니다. 상황에 맞게 선택하세요.
타입 가드와 타입 좁히기
function processValue(value: string | number | null) {
if (value === null) {
return "No value";
}
if (typeof value === "string") {
return value.toUpperCase();
}
return value * 2;
}
interface Dog {
type: "dog";
bark(): void;
}
interface Cat {
type: "cat";
meow(): void;
}
type Animal = Dog | Cat;
function makeSound(animal: Animal) {
switch (animal.type) {
case "dog":
animal.bark();
break;
case "cat":
animal.meow();
break;
}
}
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
}
}
제네릭
제네릭은 타입을 매개변수로 받아 재사용 가능한 컴포넌트를 만듭니다.
기본 제네릭
function identity<T>(arg: T): T {
return arg;
}
const str = identity<string>("hello");
const num = identity(42);
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
const lengths = map(["a", "bb", "ccc"], s => s.length);
제네릭 인터페이스와 클래스
interface Repository<T> {
findById(id: string): T | undefined;
findAll(): T[];
save(entity: T): void;
delete(id: string): void;
}
class GenericRepository<T extends { id: string }> implements Repository<T> {
private items: Map<string, T> = new Map();
findById(id: string): T | undefined {
return this.items.get(id);
}
findAll(): T[] {
return Array.from(this.items.values());
}
save(entity: T): void {
this.items.set(entity.id, entity);
}
delete(id: string): void {
this.items.delete(id);
}
}
interface User {
id: string;
name: string;
}
const userRepo = new GenericRepository<User>();
제네릭 제약 조건
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello");
logLength([1, 2, 3]);
logLength({ length: 10 });
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Kim", age: 30 };
const name = getProperty(user, "name");
const age = getProperty(user, "age");
유틸리티 타입
TypeScript는 타입 변환을 위한 내장 유틸리티 타입을 제공합니다.
Partial과 Required
interface User {
id: string;
name: string;
email: string;
age?: number;
}
type PartialUser = Partial<User>;
function updateUser(id: string, updates: Partial<User>) {
console.log(updates);
}
updateUser("1", { name: "New Name" });
type RequiredUser = Required<User>;
Pick과 Omit
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutId = Omit<User, "id">;
type CreateUserDTO = Omit<User, "id">;
type UpdateUserDTO = Partial<Omit<User, "id">>;
Record
type PageInfo = {
title: string;
url: string;
};
type Pages = Record<string, PageInfo>;
const pages: Pages = {
home: { title: "Home", url: "/" },
about: { title: "About", url: "/about" }
};
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
const rolePermissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
ReturnType과 Parameters
function createUser(name: string, age: number) {
return { id: Date.now().toString(), name, age };
}
type User2 = ReturnType<typeof createUser>;
type CreateUserParams = Parameters<typeof createUser>;
async function fetchData(): Promise<{ data: string }> {
return { data: "result" };
}
type FetchResult = Awaited<ReturnType<typeof fetchData>>;
any 타입은 타입 검사를 우회하므로 가능한 피하세요. 대신 unknown을 사용하고 타입 가드로 안전하게 처리하세요.조건부 타입
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
type B = IsString<number>;
type ExtractString<T> = T extends string ? T : never;
type OnlyStrings = ExtractString<string | number | boolean>;
type Flatten<T> = T extends Array<infer U> ? U : T;
type Num = Flatten<number[]>;
type Str = Flatten<string>;
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Data = UnwrapPromise<Promise<{ name: string }>>;
매핑된 타입
type Readonly2<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
템플릿 리터럴 타입
type EventName = `on${Capitalize<string>}`;
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/api/${"users" | "posts" | "comments"}`;
type Route = `${HttpMethod} ${ApiEndpoint}`;
type PropEventName<T extends string> = `${T}Changed`;
type UserPropEvents = PropEventName<"name" | "age" | "email">;
type CSSValue = `${number}${"px" | "em" | "rem" | "%"}`;
const width: CSSValue = "100px";
const height: CSSValue = "50%";
클래스와 타입
abstract class Entity {
abstract id: string;
abstract validate(): boolean;
}
class User3 extends Entity {
id: string;
name: string;
private password: string;
protected role: string;
readonly createdAt: Date;
constructor(id: string, name: string, password: string) {
super();
this.id = id;
this.name = name;
this.password = password;
this.role = "user";
this.createdAt = new Date();
}
validate(): boolean {
return this.name.length > 0 && this.password.length >= 8;
}
static create(name: string, password: string): User3 {
return new User3(crypto.randomUUID(), name, password);
}
}
interface Serializable {
serialize(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
class Product2 implements Serializable, Comparable<Product2> {
constructor(
public id: string,
public name: string,
public price: number
) {}
serialize(): string {
return JSON.stringify(this);
}
compareTo(other: Product2): number {
return this.price - other.price;
}
}
타입 선언 파일
전역 타입 선언
declare global {
interface Window {
myApp: {
version: string;
init(): void;
};
}
}
declare module "*.svg" {
const content: string;
export default content;
}
declare module "*.json" {
const value: unknown;
export default value;
}
모듈 확장
import "express";
declare module "express" {
interface Request {
user?: {
id: string;
role: string;
};
}
}
외부 라이브러리에 타입이 없다면
@types/ 패키지를 찾거나, 직접 d.ts 파일을 작성하세요. DefinitelyTyped 저장소에서 많은 타입 정의를 찾을 수 있습니다.실전 패턴
빌더 패턴
class QueryBuilder<T> {
private query: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.query[key] = value;
return this;
}
build(): Partial<T> {
return { ...this.query };
}
}
interface UserQuery {
name: string;
age: number;
role: string;
}
const query = new QueryBuilder<UserQuery>()
.where("name", "Kim")
.where("age", 30)
.build();
타입 안전한 이벤트 시스템
type EventMap = {
userLogin: { userId: string; timestamp: Date };
userLogout: { userId: string };
dataLoaded: { data: unknown[] };
};
class TypedEventEmitter<T extends Record<string, unknown>> {
private listeners: Map<keyof T, Set<(data: unknown) => void>> = new Map();
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener as (data: unknown) => void);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners.get(event)?.forEach(listener => listener(data));
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("userLogin", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.emit("userLogin", { userId: "123", timestamp: new Date() });
마무리
TypeScript의 타입 시스템은 강력하고 표현력이 뛰어납니다. 핵심 포인트를 정리하면 다음과 같습니다.
- 정적 타입으로 컴파일 시점에 오류 발견
- 제네릭으로 재사용 가능한 타입 안전 코드 작성
- 유틸리티 타입으로 타입 변환 간소화
- 조건부 타입과 매핑된 타입으로 고급 타입 조작
- 타입 가드로 런타임 타입 안전성 확보
다음 글에서는 TypeScript 프로젝트 설정과 고급 컴파일러 옵션에 대해 다루겠습니다.