본문으로 건너뛰기
Advertisement

7.2 고급 내장 유틸리티 타입

TypeScript는 Partial, Required, Readonly, Pick, Omit 같은 기본 유틸리티 타입 외에도, 클래스·함수·비동기 패턴을 다루기 위한 고급 유틸리티 타입을 제공합니다. 이 장에서는 Awaited, ConstructorParameters, InstanceType, ThisType, OmitThisParameter 등을 깊이 이해하고, 실전 아키텍처 패턴에서 어떻게 활용하는지 살펴봅니다.


Awaited — 재귀적 Promise 언래핑

Awaited<T>는 TypeScript 4.5에서 도입된 유틸리티 타입으로, Promise 체인을 완전히 풀어 내부 값 타입을 반환합니다. 기존의 단순한 Promise<infer V> 패턴과 달리, PromiseLike(.then 메서드만 있는 객체)도 처리합니다.

// 단일 Promise
type A1 = Awaited<Promise<string>>; // string

// 중첩 Promise — 완전히 언래핑
type A2 = Awaited<Promise<Promise<number>>>; // number

// Promise가 아닌 타입 — 그대로 반환
type A3 = Awaited<string>; // string
type A4 = Awaited<null>; // null

// async 함수와 조합
async function fetchData(): Promise<{ id: number; data: string[] }> {
return { id: 1, data: ["a", "b"] };
}

type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// { id: number; data: string[] }

// 유니온 타입도 처리
type A5 = Awaited<Promise<string> | Promise<number>>;
// string | number

// 실용적인 패턴: API 응답 타입 추출
type ApiCall<T> = () => Promise<T>;
type Unwrap<F extends () => Promise<any>> = Awaited<ReturnType<F>>;

const getUser: ApiCall<{ name: string; email: string }> = async () => ({
name: "Alice",
email: "alice@example.com",
});

type UserData = Unwrap<typeof getUser>;
// { name: string; email: string }

async 반환 타입 패턴

// async 함수들의 반환 타입을 일괄 처리
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
Awaited<ReturnType<T>>;

async function processOrder(orderId: string): Promise<{
orderId: string;
status: "pending" | "confirmed" | "shipped";
total: number;
}> {
// ...
return { orderId, status: "confirmed", total: 50000 };
}

type OrderResult = AsyncReturnType<typeof processOrder>;
// { orderId: string; status: "pending" | "confirmed" | "shipped"; total: number }

// Promise 배열의 resolved 타입
type PromiseAll<T extends readonly Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};

type Results = PromiseAll<[Promise<string>, Promise<number>, Promise<boolean>]>;
// [string, number, boolean]

ConstructorParameters — 생성자 파라미터 추출

ConstructorParameters<T>는 클래스(또는 생성자 함수)의 생성자 파라미터 타입을 튜플로 추출합니다.

class HttpClient {
constructor(
private baseUrl: string,
private timeout: number,
private headers: Record<string, string>
) {}
}

type HttpClientParams = ConstructorParameters<typeof HttpClient>;
// [baseUrl: string, timeout: number, headers: Record<string, string>]

// 구조 분해로 각 파라미터 타입 접근
type BaseUrl = HttpClientParams[0]; // string
type Timeout = HttpClientParams[1]; // number
type Headers = HttpClientParams[2]; // Record<string, string>

// 추상 클래스도 지원
abstract class Repository<T> {
constructor(
protected tableName: string,
protected db: { query: (sql: string) => Promise<T[]> }
) {}
}

type RepoParams = ConstructorParameters<typeof Repository>;
// [tableName: string, db: { query: (sql: string) => Promise<unknown[]> }]

팩토리 패턴에서의 활용

// 생성자 파라미터를 재사용한 팩토리 함수
function createInstance<T extends new (...args: any) => any>(
Constructor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Constructor(...args);
}

class Logger {
constructor(
private prefix: string,
private level: "info" | "warn" | "error"
) {}

log(message: string) {
console.log(`[${this.prefix}][${this.level}] ${message}`);
}
}

// 타입 안전한 팩토리 호출
const logger = createInstance(Logger, "App", "info");
// TS가 "App"과 "info"의 타입을 자동 검증

// 부분 적용 팩토리
function partial<T extends new (...args: any) => any>(
Constructor: T,
...preArgs: Partial<ConstructorParameters<T>>
) {
return (...remainingArgs: any[]) =>
new Constructor(...preArgs, ...remainingArgs);
}

InstanceType — 클래스 인스턴스 타입 추출

InstanceType<T>는 클래스의 new 표현식이 반환하는 타입, 즉 인스턴스 타입을 추출합니다.

class EventEmitter {
private listeners: Map<string, Function[]> = new Map();

on(event: string, listener: Function): this {
const list = this.listeners.get(event) ?? [];
this.listeners.set(event, [...list, listener]);
return this;
}

emit(event: string, ...args: any[]): void {
this.listeners.get(event)?.forEach(fn => fn(...args));
}
}

type EmitterInstance = InstanceType<typeof EventEmitter>;
// EventEmitter

// 클래스를 파라미터로 받아 인스턴스를 반환하는 함수
function getInstance<T extends new () => any>(
Cls: T
): InstanceType<T> {
return new Cls();
}

const emitter = getInstance(EventEmitter);
// 타입: EventEmitter

// 클래스 레지스트리 패턴
type Constructor<T = {}> = new (...args: any[]) => T;

class ServiceRegistry {
private services = new Map<string, any>();

register<T extends Constructor>(
name: string,
Service: T,
...args: ConstructorParameters<T>
): void {
this.services.set(name, new Service(...args));
}

get<T extends Constructor>(name: string, _type: T): InstanceType<T> {
return this.services.get(name);
}
}

// 제네릭 리포지터리 패턴
interface Entity {
id: string;
}

class BaseRepository<T extends Entity> {
protected items: T[] = [];

findById(id: string): T | undefined {
return this.items.find(item => item.id === id);
}
}

class UserRepository extends BaseRepository<{ id: string; name: string }> {
findByName(name: string) {
return this.items.filter(u => u.name === name);
}
}

type UserRepo = InstanceType<typeof UserRepository>;
// UserRepository (findById, findByName 모두 포함)

ThisType — 객체 리터럴의 this 타입 지정

ThisType<T>는 객체 리터럴 안에서 this의 타입을 지정합니다. 컴파일러 플래그 noImplicitThis와 함께 사용합니다.

// 기본 사용
interface UserMethods {
greet(): string;
updateName(newName: string): void;
}

interface UserState {
name: string;
age: number;
}

const userMethods: UserMethods & ThisType<UserState & UserMethods> = {
greet() {
// this는 UserState & UserMethods 타입
return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
},
updateName(newName: string) {
this.name = newName;
},
};

// Mixin 패턴과 ThisType
type Mixin<T, U> = T & ThisType<T & U>;

function defineMixin<T, U>(
base: T & ThisType<T & U>,
_type: U
): T & ThisType<T & U> {
return base;
}

// Vue Options API 스타일 컴포넌트 타입 (단순화)
type ComponentOptions<Data, Methods, Computed> = {
data(): Data;
methods?: Methods & ThisType<Data & Methods & { $computed: Computed }>;
computed?: {
[K in keyof Computed]: (this: Data & Methods) => Computed[K];
};
};

function defineComponent<
Data extends object,
Methods extends object,
Computed extends object
>(options: ComponentOptions<Data, Methods, Computed>): void {
// Vue 내부 처리
}

defineComponent({
data() {
return { count: 0, name: "Counter" };
},
methods: {
increment() {
this.count++; // this.count는 number 타입으로 자동 추론
},
reset() {
this.count = 0;
},
},
});

OmitThisParameter — this 파라미터 제거

TypeScript에서 함수의 첫 번째 파라미터가 this이면, 실제 호출 시에는 전달되지 않는 가짜 파라미터입니다. OmitThisParameter<T>는 함수 타입에서 this 파라미터를 제거합니다.

// this 파라미터를 가진 함수
function greet(this: { name: string }, greeting: string): string {
return `${greeting}, ${this.name}!`;
}

// this 파라미터 포함
type GreetWithThis = typeof greet;
// (this: { name: string }, greeting: string) => string

// this 파라미터 제거
type GreetWithoutThis = OmitThisParameter<typeof greet>;
// (greeting: string) => string

// bind 후에도 타입 안전성 유지
const boundGreet = greet.bind({ name: "Alice" });
// 타입: (greeting: string) => string ← this가 자동으로 제거됨

// 실용 예: 메서드를 독립 함수로 추출
class Calculator {
value: number = 0;

add(this: Calculator, n: number): Calculator {
this.value += n;
return this;
}

multiply(this: Calculator, n: number): Calculator {
this.value *= n;
return this;
}
}

type StandaloneAdd = OmitThisParameter<Calculator["add"]>;
// (n: number) => Calculator

// this 파라미터 바인딩 유틸리티
function bindMethod<T, K extends keyof T>(
instance: T,
method: K
): T[K] extends (this: T, ...args: infer A) => infer R
? (...args: A) => R
: never {
return (instance[method] as any).bind(instance);
}

const calc = new Calculator();
const standaloneAdd = bindMethod(calc, "add");
standaloneAdd(5); // OK, this 없이 호출

Parameters vs ConstructorParameters 비교

// 일반 함수 vs 클래스 생성자의 파라미터 추출 비교
function createUser(
name: string,
age: number,
role: "admin" | "user"
): { name: string; age: number; role: string } {
return { name, age, role };
}

class UserService {
constructor(
private apiUrl: string,
private maxRetries: number,
private debug: boolean
) {}
}

// 일반 함수 파라미터
type FunctionParams = Parameters<typeof createUser>;
// [name: string, age: number, role: "admin" | "user"]

// 클래스 생성자 파라미터
type ClassParams = ConstructorParameters<typeof UserService>;
// [apiUrl: string, maxRetries: number, debug: boolean]

// 비교 테이블용 차이점 시연
type ParamFirst = Parameters<typeof createUser>[0]; // string
type CtorFirst = ConstructorParameters<typeof UserService>[0]; // string

// ConstructorParameters는 abstract class도 지원
abstract class BaseService {
constructor(protected config: { timeout: number; retries: number }) {}
}

type BaseParams = ConstructorParameters<typeof BaseService>;
// [config: { timeout: number; retries: number }]

// Parameters는 오버로드된 함수에서 마지막 시그니처를 사용
function overloaded(x: string): string;
function overloaded(x: number): number;
function overloaded(x: string | number): string | number {
return x;
}

type OverloadParams = Parameters<typeof overloaded>;
// [x: string | number] ← 마지막 시그니처 기준

실전 예제 1: 의존성 주입 컨테이너

// 타입 안전한 DI 컨테이너
type Token<T> = symbol & { __type: T };

function createToken<T>(description: string): Token<T> {
return Symbol(description) as Token<T>;
}

class Container {
private bindings = new Map<symbol, any>();

bind<T>(token: Token<T>, factory: () => T): void {
this.bindings.set(token, factory);
}

bindClass<T extends new (...args: any) => any>(
token: Token<InstanceType<T>>,
Cls: T,
...deps: ConstructorParameters<T>
): void {
this.bindings.set(token, () => new Cls(...deps));
}

get<T>(token: Token<T>): T {
const factory = this.bindings.get(token);
if (!factory) throw new Error(`No binding for token`);
return factory();
}
}

// 서비스 정의
class DatabaseService {
constructor(
private host: string,
private port: number
) {}

query(sql: string): Promise<any[]> {
return Promise.resolve([]);
}
}

class UserService {
constructor(private db: DatabaseService) {}

async findUser(id: string) {
return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
}
}

// 토큰 생성
const DB_TOKEN = createToken<DatabaseService>("DatabaseService");
const USER_TOKEN = createToken<UserService>("UserService");

// 컨테이너 설정
const container = new Container();
container.bindClass(DB_TOKEN, DatabaseService, "localhost", 5432);

container.bind(USER_TOKEN, () => {
const db = container.get(DB_TOKEN);
return new UserService(db);
});

// 타입 안전한 의존성 해결
const userService = container.get(USER_TOKEN);
// 타입: UserService

실전 예제 2: Mixin 패턴 타입 안전성

// 타입 안전한 Mixin 구현
type GConstructor<T = {}> = new (...args: any[]) => T;
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;

// Serializable Mixin
function Serializable<TBase extends GConstructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}

static deserialize<T extends { new(data: any): any }>(
this: T,
data: string
): InstanceType<T> {
return Object.assign(new this({}), JSON.parse(data));
}
};
}

// Timestamped Mixin
function Timestamped<TBase extends GConstructor>(Base: TBase) {
return class extends Base {
createdAt: Date = new Date();
updatedAt: Date = new Date();

touch(): void {
this.updatedAt = new Date();
}
};
}

// Activatable Mixin
function Activatable<TBase extends GConstructor>(Base: TBase) {
return class extends Base {
isActive: boolean = false;

activate(): void {
this.isActive = true;
}

deactivate(): void {
this.isActive = false;
}
};
}

// 기본 클래스
class BaseModel {
constructor(public id: string) {}
}

// 믹스인 조합
const TimestampedModel = Timestamped(BaseModel);
const ActiveTimestampedModel = Activatable(TimestampedModel);
const FullModel = Serializable(ActiveTimestampedModel);

type FullModelInstance = InstanceType<typeof FullModel>;
// id, createdAt, updatedAt, touch, isActive, activate, deactivate, serialize 모두 포함

class UserModel extends FullModel {
constructor(id: string, public name: string, public email: string) {
super(id);
}
}

const user = new UserModel("u1", "Alice", "alice@example.com");
user.activate();
user.touch();
const serialized = user.serialize();
console.log(serialized);

고수 팁

유틸리티 타입 체이닝

// 여러 유틸리티 타입을 체인으로 연결
class ApiService {
constructor(
private baseUrl: string,
private apiKey: string
) {}

async get<T>(path: string): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
headers: { "X-API-Key": this.apiKey },
});
return res.json();
}
}

// 복잡한 체이닝: 생성자 파라미터 → 부분 선택 → readonly
type ApiServiceConfig = Readonly<
Pick<
// ConstructorParameters를 객체로 변환
{ baseUrl: string; apiKey: string },
"baseUrl"
>
>;
// { readonly baseUrl: string }

// 클래스 메서드 타입 추출 체이닝
type GetMethodReturn = Awaited<
ReturnType<InstanceType<typeof ApiService>["get"]>
>;
// unknown (제네릭 T가 unknown으로 추론)

복잡한 타입 단순화 전략

// 단계별 타입 분해로 가독성 향상
type ComplexType =
Awaited<
ReturnType<
InstanceType<
typeof UserService
>["findUser"]
>
>;

// 단계별 중간 타입 정의 (디버깅과 재사용에 유리)
type UserServiceInstance = InstanceType<typeof UserService>;
type FindUserReturn = ReturnType<UserServiceInstance["findUser"]>;
type FindUserResult = Awaited<FindUserReturn>;

// 타입 검증 유틸리티
type Assert<T extends true> = T;
type IsEqual<A, B> = A extends B ? (B extends A ? true : false) : false;

// 두 타입이 같은지 확인
type Check = Assert<IsEqual<FindUserResult, ComplexType>>;
// 컴파일되면 두 타입이 동일

// Prettify: 복잡한 인터섹션 타입을 단순한 객체 타입으로 표시
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};

type MixedType = { a: string } & { b: number } & { c: boolean };
type PrettyMixed = Prettify<MixedType>;
// { a: string; b: number; c: boolean } ← IDE에서 보기 쉽게 표시

정리

유틸리티 타입대상반환주요 용도
Awaited<T>Promise/PromiseLike내부 값 타입async 반환 타입 추출
ConstructorParameters<T>클래스/생성자 함수파라미터 튜플팩토리, DI 컨테이너
InstanceType<T>클래스/생성자 함수인스턴스 타입제네릭 팩토리, Mixin
ThisType<T>객체 리터럴this 타입 지정Options API, Mixin
OmitThisParameter<T>this 파라미터 포함 함수this 제거된 함수 타입bind, 메서드 추출
Parameters<T>일반 함수파라미터 튜플함수 래핑, 커링

다음 장에서는 TypeScript 4.9에서 도입된 satisfies 연산자를 살펴봅니다. 타입 단언(as)과 타입 어노테이션의 단점을 모두 보완하는 이 연산자로 더 안전하고 정확한 타입 추론을 달성하는 방법을 배웁니다.

Advertisement