기본 패턴
// interface는 같은 이름으로 여러 번 선언하면 자동으로 병합된다
interface User {
id: number;
name: string;
}
interface User {
email: string;
role: "admin" | "user";
}
// 결과: id, name, email, role 모두 포함
const user: User = {
id: 1,
name: "Kim",
email: "kim@example.com",
role: "admin",
};설명
interface는 같은 이름으로 여러 번 선언하면 자동으로 병합된다 — type alias는 이것이 불가능하다
TypeScript에서 interface는 동일한 이름으로 여러 파일이나 같은 파일에서 반복 선언할 수 있으며, 컴파일러가 이를 하나의 타입으로 합쳐준다. 이를 선언 병합(declaration merging)이라 한다. 각 선언의 멤버가 모두 최종 타입에 포함된다. type 키워드로 만든 타입 별칭은 동일한 이름으로 재선언하면 오류가 발생하므로 병합이 불가능하다. 선언 병합은 라이브러리 타입을 건드리지 않고 외부에서 확장하거나, 기능 플래그에 따라 조건부로 타입을 추가해야 할 때 특히 유용하다.
// ✅ interface 병합 — 두 선언이 합쳐진다
interface Config {
host: string;
port: number;
}
interface Config {
timeout: number;
retries: number;
}
const config: Config = {
host: "localhost",
port: 3000,
timeout: 5000,
retries: 3,
};
// ❌ type alias는 병합 불가 — 오류
type Settings = { host: string };
type Settings = { port: number }; // Error: Duplicate identifier 'Settings'
// 병합 시 메서드 시그니처 오버로드도 가능
interface Formatter {
format(value: string): string;
}
interface Formatter {
format(value: number): string; // 오버로드 추가
}모듈 보강(module augmentation)으로 서드파티 라이브러리 타입을 확장한다 — declare module '...' 패턴
라이브러리 타입 정의 파일을 직접 수정하면 패키지 업데이트 시 변경이 사라진다. 모듈 보강은 declare module '패키지명' 블록 안에서 해당 패키지의 인터페이스를 재선언해 타입을 추가하는 방법이다. 이렇게 하면 원본 타입 정의를 건드리지 않고 프로젝트 코드에서 커스텀 프로퍼티나 메서드를 타입 안전하게 추가할 수 있다. 파일이 모듈이 되려면 최소 하나의 import 또는 export 문이 있어야 한다.
// types/axios-augmentation.d.ts
import "axios"; // 이 파일을 모듈로 만드는 핵심
declare module "axios" {
export interface AxiosRequestConfig {
// 커스텀 메타데이터 필드 추가
metadata?: {
startTime?: number;
requestId?: string;
};
}
}
// 이제 타입 오류 없이 사용 가능
import axios from "axios";
axios.get("/api/data", {
metadata: {
startTime: Date.now(),
requestId: crypto.randomUUID(),
},
});namespace 병합으로 함수·클래스에 정적 프로퍼티 타입을 추가한다
TypeScript에서 namespace는 함수나 클래스와 같은 이름으로 선언하면 병합된다. 이를 활용하면 함수 자체를 호출하는 타입과 함수의 정적 프로퍼티를 별도로 선언할 수 있다. 라이브러리에서 함수를 내보내면서 동시에 그 함수에 연관 상수나 타입을 달아둘 때 자주 쓰이는 패턴이다.
// 함수 선언
function createValidator(schema: object) {
return function validate(data: unknown): boolean {
// 검증 로직
return true;
};
}
// 같은 이름의 namespace로 정적 멤버 타입 추가
namespace createValidator {
export interface Options {
strict: boolean;
allowUnknown: boolean;
}
export const defaultOptions: Options = {
strict: false,
allowUnknown: true,
};
}
// 사용 측
const options: createValidator.Options = {
strict: true,
allowUnknown: false,
};
// class와 namespace 병합으로 정적 팩토리 메서드 타입 추가
class Color {
constructor(public r: number, public g: number, public b: number) {}
}
namespace Color {
export function fromHex(hex: string): Color {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return new Color(r, g, b);
}
}
const red = Color.fromHex("#ff0000"); // 타입 안전declare global로 전역 타입을 확장한다 — Express req.user 패턴이 대표적이다
Express 미들웨어에서 req.user에 인증 사용자 정보를 붙이는 패턴은 매우 흔하지만, 기본 Request 타입에는 user 프로퍼티가 없어 타입 오류가 발생한다. declare global 블록 안에서 전역 네임스페이스 Express의 Request 인터페이스를 보강하면 미들웨어 이후 모든 핸들러에서 req.user를 타입 안전하게 사용할 수 있다. declare global은 반드시 모듈 파일(import/export 존재) 안에서 써야 한다.
// types/express-augmentation.d.ts
import { JwtPayload } from "jsonwebtoken"; // 이 파일을 모듈로 만듦
declare global {
namespace Express {
interface Request {
user?: {
id: number;
email: string;
role: "admin" | "user";
};
requestId?: string;
}
}
}
// middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
req.user = { id: payload.id, email: payload.email, role: payload.role };
next();
}
// routes/profile.ts
import { Request, Response } from "express";
export function getProfile(req: Request, res: Response) {
// req.user 타입이 안전하게 추론됨
if (!req.user) return res.status(401).json({ error: "Unauthorized" });
res.json({ email: req.user.email, role: req.user.role });
}빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| 같은 프로젝트 내 interface를 여러 곳에서 확장 | 선언 병합 (동일 이름 interface 재선언) |
| 서드파티 패키지 타입에 프로퍼티 추가 | declare module '패키지명' 모듈 보강 |
| 함수·클래스에 정적 멤버 타입 추가 | 동일 이름 namespace 병합 |
window, process 등 전역 타입 확장 | declare global (모듈 파일 안에서) |
Express req, res에 커스텀 프로퍼티 | declare global { namespace Express { ... } } |
주의할 점
모듈 보강과 declare global은 파일이 반드시 모듈이어야 동작합니다.
import/export가 없는 script 파일에서는 동작하지 않습니다.
// ❌ import/export 없는 파일에서 declare module 사용 — 효과 없음
declare module "axios" {
interface AxiosRequestConfig {
metadata?: object;
}
}
// ✅ 최소 하나의 import/export로 모듈로 만든 후 사용
export {}; // 빈 export로도 모듈로 만들 수 있다
declare module "axios" {
interface AxiosRequestConfig {
metadata?: object;
}
}
// ❌ script 파일에서 declare global 사용 — 전역 오염 위험
declare global {
interface Window {
analytics: AnalyticsInstance;
}
}
// ✅ 모듈 파일 안에서 declare global 사용
export {};
declare global {
interface Window {
analytics: AnalyticsInstance;
}
}보강 파일이 tsconfig의 include 또는 typeRoots에 포함되어야 TypeScript가 인식합니다.
파일을 만들었는데 타입이 적용되지 않는다면 tsconfig 설정을 먼저 확인하세요.