고급 TypeScript 패턴: 스케일링 가능한 타입 안전 코드
Source: Dev.to

제네릭 제약과 추론
제약이 있는 기본 제네릭
// Constrain a generic to objects that have an `id` property
interface HasId {
id: string | number;
}
/**
* Find an element by its `id`.
* @param items - Array of items that extend `HasId`
* @param id - The id to look for (must be the same type as the item's id)
*/
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
return items.find(item => item.id === id);
}
// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' },
];
const user = findById(users, 1); // Type: User | undefined
제네릭 팩토리 함수
/**
* Simple store implementation with type‑safe state handling.
* @param initialState - The initial state of the store; its type is inferred.
*/
function createStore<T>(initialState: T) {
let state = initialState;
const listeners = new Set<(s: T) => void>();
return {
/** Returns the current state */
getState: () => state,
/**
* Merges a partial update into the current state.
* @param newState - Partial object containing the fields to update.
*/
setState: (newState: Partial<T>) => {
state = { ...state, ...newState };
listeners.forEach(listener => listener(state));
},
/**
* Subscribes a listener that will be called on every state change.
* @param listener - Function receiving the new state.
* @returns A function to unsubscribe the listener.
*/
subscribe: (listener: (state: T) => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
id: number;
name: string;
email: string;
}
const userStore = createStore({
user: null as User | null,
isLoading: false,
error: null as string | null,
});
userStore.setState({ isLoading: true }); // ✅ Type‑safe
Tip: TypeScript는 사용으로부터 복잡한 타입을 추론할 수 있습니다. 내부 구현에서는 컴파일러에게 무거운 작업을 맡기되, 공개 API에서는 항상 명시적인 제네릭 제약(e.g.,
T extends HasId)을 추가하여 명확하고 안전하게 유지하세요.
Source:
조건부 타입
기본 조건부 타입
// Generic conditional type that maps a key to a response type
type ApiResponse<T> = T extends 'user'
? User
: T extends 'product'
? Product
: never;
// Example payload types
interface User {
id: number;
name: string;
}
interface Product {
id: number;
price: number;
}
// Usage
type UserResponse = ApiResponse<'user'>; // → User
type ProductResponse = ApiResponse<'product'>; // → Product
실용적인 예 – Promise 타입 풀기
// Simplified version of the built‑in `Awaited` type
type Unwrap<T> = T extends Promise<infer R> ? Unwrap<R> : T;
// Example
type Result = Unwrap<Promise<Promise<string>>>; // → string
분배형 조건부 타입
조건부 타입은 유니온 타입에 자동으로 분배됩니다. 이를 통해 유니온의 각 멤버를 개별적으로 필터링하거나 변환하는 작업이 편리해집니다.
null / undefined 제거
// Remove `null` and `undefined` from a type
type NonNullable<T> = T extends null | undefined ? never : T;
// Example
type Example = NonNullable<string | null | undefined>; // → string
유니온에서 특정 멤버 추출
// Keep only the `string` members of a union
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = string | number | boolean | 'hello' | 'world';
type OnlyStrings = ExtractStrings<Mixed>; // → string | 'hello' | 'world'
Note:
ExtractStrings<T>조건부 타입이Mixed의 각 멤버에 개별적으로 적용되기 때문에,extends string검사를 통과한 멤버들의 유니온이 결과로 반환됩니다. 이 분배형 동작이 TypeScript에서 강력한 타입‑레벨 필터링을 가능하게 합니다.
타입 추출을 위한 infer 키워드
infer 키워드는 조건부 타입 내부에서 타입을 캡처하여 다른 곳에서 재사용할 수 있게 해줍니다. 아래는 타입에서 정보를 추출하는 가장 흔한 패턴들입니다.
// ------------------------------------------------------------
// 1️⃣ 함수의 매개변수 타입 추출
// ------------------------------------------------------------
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// ------------------------------------------------------------
// 2️⃣ 함수의 반환 타입 추출
// ------------------------------------------------------------
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
// ------------------------------------------------------------
// 3️⃣ 배열(또는 튜플)의 요소 타입 추출
// ------------------------------------------------------------
type ArrayElement<T> = T extends (infer E)[] ? E : never;
// ------------------------------------------------------------
// 4️⃣ React 컴포넌트의 props 타입 추출
// ------------------------------------------------------------
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
📚 실용 예제
// 간단한 async 함수
async function fetchUser(id: number): Promise<User> {
/* … */
}
// 위에서 정의한 헬퍼 사용
type FetchUserParams = Parameters<typeof fetchUser>; // => [number]
type FetchUserReturn = Return<typeof fetchUser>; // => Promise<User>
헬퍼가 하는 일
| 헬퍼 | 입력 | 추출된 타입 |
|---|---|---|
Parameters<T> | (...args: infer P) => any | P – 함수 매개변수들의 튜플 |
Return<T> | (...args: any[]) => infer R | R – 함수의 반환 타입 |
ArrayElement<T> | (infer E)[] | E – 배열 각 요소의 타입 |
ComponentProps<T> | React.ComponentType<infer P> | P – 컴포넌트의 props 타입 |
이 유틸리티들은 TypeScript에서 고차 함수, 타입이 지정된 래퍼, 혹은 제네릭 컴포넌트 라이브러리와 같은 고급 타입‑레벨 패턴을 구현하기 위한 기본 블록입니다.
Source: …
매핑된 타입
객체 타입 변환
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Pick specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
고급 매핑된 타입
// Create getters for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
/* Example */
type PersonGetters = Getters<Person>;
/* Result:
{
getName: () => string;
getAge: () => number;
}
*/
// Filter properties by their value type
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
/* Example */
type NumberProps = FilterByType<Mixed, number>;
/* Result:
{
id: number;
count: number;
}
*/
Note: 템플릿 리터럴 타입과 매핑된 타입을 결합하면 강력한 타입 변환을 만들 수 있습니다—강력히 타입이 지정된 API 클라이언트, 폼 빌더 및 기타 메타프로그래밍 시나리오를 생성하는 데 완벽합니다.
타입 가드와 좁히기
사용자 정의 타입 가드
// Type predicate function
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value &&
typeof (value as User).id === "number" &&
typeof (value as User).email === "string"
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows `data` is `User` here
console.log(`User ${data.name} (${data.id})`);
} else {
console.warn("Not a user");
}
}
타입 가드가 있는 차별화된 유니온
interface SuccessResponse {
status: 'success';
data: User;
}
interface ErrorResponse {
status: 'error';
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
// TypeScript narrows to SuccessResponse
console.log(response.data.name);
} else {
// TypeScript narrows to ErrorResponse
console.log(response.message);
}
}
Assertion Functions
/**
* 제공된 값이 `User`인지 확인합니다.
* 검사에 실패하면 오류를 발생시켜 TypeScript가 타입을 좁히도록 합니다.
*/
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Value is not a User');
}
}
/** 사용 예시 */
function processUserData(data: unknown) {
// 단언 후 `data`를 `User`로 좁힘
assertIsUser(data);
console.log(data.email); // 이제 `data`는 `User`임이 알려짐
}
/**
* 값이 `null`도 `undefined`도 아닌지 확인합니다.
*
* @param value 확인할 값.
* @param message 선택적인 사용자 정의 오류 메시지.
*
* @throws `value`가 `null`이거나 `undefined`인 경우 오류를 발생시킵니다.
*/
function assertDefined<T>(
value: T | null | undefined,
message?: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(message ?? 'Value is not defined');
}
}
템플릿 리터럴 타입
원본 섹션에 제공된 코드 예제가 없습니다.
타입‑안전 API 구축
// ------------------------------------------------------------
// 1️⃣ Event handler types
// ------------------------------------------------------------
// Allowed event names
type EventName = 'click' | 'focus' | 'blur';
// Utility type that builds the handler name (e.g. "onClick")
type EventHandler<E extends EventName> = `on${Capitalize<E>}`;
// Example usage
type ClickHandler = EventHandler<'click'>; // "onClick"
type FocusHandler = EventHandler<'focus'>; // "onFocus"
type BlurHandler = EventHandler<'blur'>; // "onBlur"
// ------------------------------------------------------------
// 2️⃣ Route parameters extraction
// ------------------------------------------------------------
// Route patterns
type Route =
| '/users/:id'
| '/posts/:postId/comments/:commentId';
/**
* Recursively extracts all `:param` placeholders from a route string.
*
* @template T - The route string to analyse.
* @returns A union of the extracted parameter names.
*/
type ExtractParams<T extends string> =
// Match a segment that contains a parameter followed by more segments
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
// Match the final segment that contains a parameter
: T extends `${infer _Start}:${infer Param}`
? Param
// No more parameters
: never;
// Example extractions
type UserRouteParams = ExtractParams<'/users/:id'>; // "id"
type CommentRouteParams = ExtractParams<'/posts/:postId/comments/:commentId'>;
// → "postId" | "commentId"
위 스니펫은 다음을 보여줍니다:
- 템플릿 리터럴 타입과
Capitalize를 사용해 타입‑안전 이벤트‑핸들러 이름 생성 - URL 패턴에서 라우트 파라미터를 추출하여 플레이스홀더 이름의 유니온을 얻기
타입‑안전 CSS 속성
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;
interface Spacing {
margin: CSSValue;
padding: CSSValue;
}
const spacing: Spacing = {
margin: '16px', // Valid
padding: '1.5rem', // Valid
// margin: '16', // Error: not a valid CSSValue
};
실전에서의 유틸리티 타입
(섹션이 유지되었습니다; 구체적인 코드는 제공되지 않았습니다.)
Form 라이브러리 만들기
아래는 작고 타입‑안전한 폼‑구성 도우미입니다. 데이터 모델의 형태를 기반으로 적절한 입력 타입을 추론하고, 라벨, 검증, 필수 플래그를 추가할 수 있습니다.
// ------------------------------------------------------------
// 1️⃣ Generic field‑configuration type
// ------------------------------------------------------------
type FieldConfig<T> = {
[K in keyof T]: {
/** Input type inferred from the property type */
type:
T[K] extends string
? 'text' | 'email' | 'password'
: T[K] extends number
? 'number'
: T[K] extends boolean
? 'checkbox'
: 'text';
/** Human‑readable label */
label: string;
/** Optional “required” flag */
required?: boolean;
/** Optional validator – returns an error string or `undefined` */
validate?: (value: T[K]) => string | undefined;
};
};
// ------------------------------------------------------------
// 2️⃣ Example data model
// ------------------------------------------------------------
interface UserForm {
name: string;
email: string;
age: number;
newsletter: boolean;
}
// ------------------------------------------------------------
// 3️⃣ Concrete configuration for `UserForm`
// ------------------------------------------------------------
const userFormConfig: FieldConfig<UserForm> = {
name: {
type: 'text',
label: 'Full Name',
required: true,
validate: value =>
}
}