ThisType과 OmitThisParameter 완전 정복
TypeScript의 ThisType<T>와 OmitThisParameter<F>를 깊이 파헤칩니다. 객체 리터럴 메서드에서 this 타입을 제어하는 원리, Vue 2 Options API 구현 패턴, bind 후 this 제거, noImplicitThis 설정과의 관계를 정리합니다.
지난 글에서 생성자 타입을 다루는 방법을 살펴봤습니다. 이번에는 this 컨텍스트를 타입 레벨에서 다루는 두 가지 특수 유틸리티 ThisType<T>와 OmitThisParameter<F>를 정리합니다. 이 두 타입은 다른 유틸리티에 비해 덜 알려져 있지만, 객체 리터럴 기반 API 설계와 함수 바인딩에서 필수적입니다.
this 파라미터란?
TypeScript는 함수의 첫 번째 파라미터에 this라는 이름을 붙여 해당 함수 내 this의 타입을 선언할 수 있습니다. 이 파라미터는 컴파일 후 사라집니다.
interface User {
id: number;
name: string;
}
// this 파라미터 선언
function greet(this: User, greeting: string): string {
return `${greeting}, ${this.name}!`;
}
// 잘못된 this로 호출하면 에러
greet.call({ id: 1, name: "Alice" }, "Hello"); // OK
greet("Hello"); // Error: this 컨텍스트가 없음
noImplicitThis: true(또는 strict: true)를 사용하면 this 타입이 불명확할 때 에러를 발생시킵니다.
ThisType<T>: 객체 리터럴의 this 타입 제어
ThisType<T>는 런타임 영향이 없는 마커 타입입니다. 객체 리터럴 컨텍스트에서 내부 메서드들의 this 타입을 지정하는 데 사용합니다.
interface Todo {
title: string;
completed: boolean;
}
interface TodoMethods {
toggle(): void;
getTitle(): string;
}
// ThisType을 사용해 methods 내 this의 타입을 명시
const todoConfig: {
data: Todo;
methods: TodoMethods & ThisType<Todo & TodoMethods>;
} = {
data: { title: "Learn TypeScript", completed: false },
methods: {
toggle() {
this.completed = !this.completed; // this: Todo & TodoMethods
},
getTitle() {
return this.title; // this.title — string
}
}
};
ThisType이 없으면 methods 내부의 this는 {} 또는 any가 되어 this.title에 접근할 때 에러가 납니다.
Vue 2 Options API 구현
Vue 2가 TypeScript에서 Options API를 타입 안전하게 지원할 때 ThisType을 핵심적으로 활용합니다.
type ComponentOptions<D, M, C> = {
data(): D;
methods?: M & ThisType<D & M & C>;
computed?: C & ThisType<D & M & C>;
};
function defineComponent<D, M, C>(
options: ComponentOptions<D, M, C>
): ComponentOptions<D, M, C> {
return options;
}
const Counter = defineComponent({
data() {
return { count: 0, step: 1 };
},
methods: {
increment() {
this.count += this.step; // this: { count: number; step: number; ... }
},
reset() {
this.count = 0;
}
}
});
실전 패턴
플루언트 인터페이스(Builder 패턴)
interface QueryBuilder {
where(condition: string): this;
limit(n: number): this;
offset(n: number): this;
build(): string;
}
class SqlBuilder implements QueryBuilder {
private conditions: string[] = [];
private _limit = 100;
private _offset = 0;
where(condition: string): this {
this.conditions.push(condition);
return this; // this 반환 → 체이닝 가능
}
limit(n: number): this {
this._limit = n;
return this;
}
offset(n: number): this {
this._offset = n;
return this;
}
build(): string {
const where = this.conditions.join(" AND ");
return `SELECT * FROM users WHERE ${where} LIMIT ${this._limit} OFFSET ${this._offset}`;
}
}
OmitThisParameter<F>: this 파라미터 제거
OmitThisParameter<F>는 함수 타입 F에서 this 파라미터를 제거한 타입을 반환합니다.
function format(this: User, template: string): string {
return template.replace("{name}", this.name);
}
// this 파라미터가 있는 타입
type WithThis = typeof format; // (this: User, template: string) => string
// this 파라미터를 제거한 타입
type WithoutThis = OmitThisParameter<typeof format>; // (template: string) => string
bind와의 조합
Function.prototype.bind의 반환 타입이 내부적으로 OmitThisParameter를 사용합니다.
const user: User = { id: 1, name: "Alice" };
// bind로 this를 고정하면 이후 this 파라미터가 없어짐
const boundFormat = format.bind(user);
// boundFormat: (template: string) => string — OmitThisParameter가 적용된 타입
boundFormat("{name}님 환영합니다"); // "Alice님 환영합니다"
이벤트 핸들러에서 this 금지
class EventEmitter {
// this: void를 사용하면 메서드 내에서 this 접근 자체를 금지
emit(this: void, event: string): void {
console.log(event);
}
}
const emitter = new EventEmitter();
const handler = emitter.emit; // OmitThisParameter<typeof emitter.emit>
handler("click"); // this 없이 호출 가능
내부 구현
type OmitThisParameter<T> =
unknown extends ThisParameterType<T>
? T
: T extends (...args: infer A) => infer R
? (...args: A) => R
: T;
type ThisParameterType<T> =
T extends (this: infer U, ...args: any) => any ? U : unknown;
ThisParameterType<T>는 함수의 this 파라미터 타입을 추출하고, OmitThisParameter는 이를 활용해 this 없는 함수 타입을 재구성합니다.
지난 글: InstanceType과 ConstructorParameters 완전 정복
다음 글: Deep Readonly 패턴 완전 정복
읽어주셔서 감사합니다. 😊