一.this 도 일종의 타입입니다!
class BasicDOMNode {
constructor(private el: Element) { }
addClass(cssClass: string) {
this.el.classList.add(cssClass);
return this;
}
}
class DOMNode extends BasicDOMNode {
addClasses(cssClasses: string[]) {
for (let cssClass of cssClasses) {
this.addClass(cssClass);
}
return this;
}
}
그 중에서, addClass 와 addClasses 의 타입 시그니처는 각각:
addClass(cssClass: string): this
addClasses(cssClasses: string[]): this
반환 타입은 this 로, 소속 클래스 또는 인터페이스의 서브타입을 나타냅니다 (이를 유계 다형성 (F-bounded polymorphism) 이라고 합니다). 예를 들어:
let node = new DOMNode(document.querySelector('div'));
node
.addClass('page')
.addClasses(['active', 'spring'])
.addClass('first')
위의 체인 호출 중에서, this 타입은 자동으로 소속 클래스 인스턴스 타입에 대응할 수 있습니다. 맞습니다, 이 JavaScript 런타임 특성은, TypeScript 정적 타입 시스템에서도 동일하게 서포트됩니다
구체적으로는, TypeScript 중의 this 타입은 2 종류로 나뉩니다:
-
class this type: 클래스/인터페이스 (의 멤버 메서드) 중의 this 타입
-
function this type: 일반 함수 중의 this 타입
二.Class this type
JavaScript Class 중의 this
// JavaScript
class A {
foo() { return this }
}
class B extends A {
bar() { return this }
}
new B().foo().bar();
위의 체인 호출은 정상적으로 실행되며, 마지막에 B 클래스 인스턴스를 반환합니다. 실행 시 this 가 현재 클래스 또는 그 서브클래스 인스턴스를 가리키는 것은, JavaScript 런타임에서는 매우 일반적인 동작입니다
즉, this 의 타입은 고정되지 않으며, 그 호출 컨텍스트에 의존합니다. 예를 들어:
// A 클래스 인스턴스 타입
new A().foo();
// B 클래스 인스턴스 타입
new B().foo();
// B 클래스 인스턴스 타입
new A().foo.call(new B());
Class A 중의 this 는 항상 A 클래스 인스턴스를 가리키는 것은 아닙니다 (A 의 서브클래스 인스턴스일 수도 있습니다). 그렇다면, 어떻게 this 의 타입을 기술해야 할까요?
this 의 타입
최초의 시나리오에 타입 기술을 추가하는 경우, 이렇게 시도할지도 모릅니다 (class this type 이 없다면):
declare class A {
foo(): A;
}
declare class B extends A {
bar(): B;
}
// 에러 Property 'bar' does not exist on type 'A'.
new B().foo().bar();
예상通りの 결과로, foo(): A 는 A 클래스 인스턴스를 반환하므로, 물론 서브클래스 B 의 멤버 메서드를 찾을 수 없습니다. 실제로 기대하는 것은:
A 클래스 인스턴스 타입, foo() 메서드를 가짐
|
new B().foo().bar()
|
B 클래스 인스턴스 타입, bar() 메서드를 가짐
그럼, 더욱 시도합니다:
declare class A {
foo(): A & B;
}
declare class B extends A {
bar(): B & A;
}
new B().foo().bar();
B 클래스 중의 this 는 B 클래스 인스턴스이기도 하고 A 클래스 인스턴스이기도 합니다. 일단 bar(): B & A 는 적절하다고 생각되지만, 无论如何 foo(): A & B 는 불합리합니다. 왜냐하면 기반 클래스 인스턴스는 반드시 서브클래스 인스턴스는 아니기 때문입니다……this 에 적절한 타입을 라벨할 방법이 없는 것 같습니다. 특히 superThis.subMethod() 의 시나리오에서는
따라서, 유사한 시나리오에 대해, 특수한 타입을 도입할 필요가 있습니다. 즉 this 타입입니다:
Within a class this would denote a type that behaves like a subtype of the containing class (effectively like a type parameter with the current class as a constraint).
this 타입은 소속 클래스/인터페이스의 서브타입으로서 행동하며, 이는 JavaScript 런타임의 this 값 메커니즘과 일치합니다. 예를 들어:
class A {
foo(): this { return this }
}
class B extends A {
bar(): this { return this }
}
new B().foo().bar()
즉, this 타입은 this 값의 타입입니다:
In a non-static member of a class or interface, this in a type position refers to the type of this.
구현 원리
The polymorphic this type is implemented by providing every class and interface with an implied type parameter that is constrained to the containing type itself.
간단히 말해, 클래스/인터페이스를 암묵의 타입 파라미터 this 를 가진 제네릭으로 보고, 그 소재하는 클래스/인터페이스 관련의 타입 제약을 추가합니다
Consider every class/interface as a generic type with an implicit this type arguments. The this type parameter is constrained to the type, i.e.
A<this extends A<A>>. The type of the value this inside a class or an interface is the generic type parameter this. Every reference to class/interface A outside the class is a type reference toA<this: A>. assignment compatibility flows normally like other generic type parameters.
구체적으로는, this 타입은 구현상 A<this extends A<A>> 에 상당합니다 (즉 고전적인 CRTP 기이 재귀 템플릿 패턴). 클래스 중 this 값의 타입은 제네릭 파라미터 this 입니다. 현재의 클래스/인터페이스의 컨텍스트를 나오면, this 의 타입은 A<this: A> 가 되며, 타입 호환성 등은 제네릭과 일치합니다
따라서, this 타입은 클래스 파생 관계 [제약](/articles/제네릭-typescript 노트 6/#articleHeader8) 을 가진 암묵의 [타입 파라미터](/articles/제네릭-typescript 노트 6/#articleHeader3) 와 같은 것입니다
三.Function this type
클래스/인터페이스之外, this 타입은 일반 함수에도 적용됩니다
class this type 이 통상 암묵으로 기능하는 (예를 들어 자동 [타입 추론](/articles/深入 타입 시스템-typescript 노트 8/#articleHeader1)) 와는 달리, function this type 은 대부분명시적 선언을 통해 함수 본체 중의 this 값의 타입을 제약합니다:
This-types for functions allows Typescript authors to specify the type of this that is bound within the function body.
구현 원리
this 를 명시적으로 함수의 (첫 번째) 파라미터로서, 그 타입을 한정하고, 일반 파라미터와 마찬가지로 타입 체크를 수행합니다. 예를 들어:
declare class C { m(this: this); }
let c = new C();
// f 타입은 (this:C) => any
let f = c.m;
// 에러 The 'this' context of type 'void' is not assignable to method's 'this' of type 'C'.
f();
주의. 명시적으로 this 값 타입을 선언한 경우에만 체크를 수행합니다 (위의 예와 같이):
// 명시적으로 선언된 this 타입을 삭제
declare class C { m(); }
let c = new C();
// f 타입은 () => any
let f = c.m;
// 올바른
f();
P.S. 特殊的에는, 애로우 함수 (lambda) 의 this 는 수동으로 그 타입을 한정할 수 없습니다:
let obj = {
x: 1,
// 에러 An arrow function cannot have a 'this' parameter.
f: (this: { x: number }) => this.x
};
class this type 과의 관련
멤버 메서드도 동시에 함수이며, 2 개의 this 타입은 여기서 교차합니다:
If this is not provided, this is the class' this type for methods.
즉, 멤버 메서드 중에서, function this type 을 제공하지 않는 경우, 그 클래스/인터페이스의 class this type 을沿用합니다. 자동 추론으로부터 온 타입과 명시적 선언 타입의 관계와 유사합니다: 후자는 전자를 오버라이트할 수 있습니다
주의. 당초의 설계는 이랬지만 (strictThis/strictThisChecks 옵션을 온), 퍼포먼스 등의 이유로, 후에 이 옵션을 삭제했습니다. 따라서, 현재 function this type 과 class this type 의 암묵 체크는 모두 매우 약합니다 (예를 들어 명시적으로 this 타입을 지정하지 않는 멤버 메서드는 디폴트로 class this type 제약을 가지지 않습니다)
class C {
x = { y: 1 };
f() { return this.x; }
}
let f = new C().f;
// 올바른
f();
그 중에서 f 의 타입은 () => { y: number; } 로, 기대되는 (this: C) => { y: number; } 가 아닙니다
四.적용 시나리오
유체 인터페이스 (Fluent interface)
this 타입은 유체 인터페이스 (fluent interface) 를 매우 간단하게 기술할 수 있습니다. 예를 들어:
class A {
foo(): this { return this }
}
class B extends A {
bar(): this { return this }
}
new B().foo().bar()
P.S. 소위유체 인터페이스 (설계 레벨) 는, 간단히 체인 호출 (구현 레벨) 이라고 이해할 수 있습니다:
A fluent interface is a method for designing object oriented APIs based extensively on method chaining with the goal of making the readability of the source code close to that of ordinary written prose, essentially creating a domain-specific language within the interface.
(Fluent interface 에서 발췌)
간단히 말해, 유체 인터페이스는 OOP 중의 일종 API 설계 방식으로, 체인 메서드 호출을 통해 소스 코드의 가독성을 극히 높입니다
this 의 타입을 기술
function this type 은 우리가 일반 파라미터와 마찬가지로 this 의 타입을 한정하는 것을 허가하며, 이는 Callback 시나리오에서 특히 중요합니다:
class Cat {
constructor(public name: string) {}
meow(this: Cat) { console.log('meow~'); }
}
class EventBus {
on(type: string, handler: (this: void, ...params) => void) {/* ... */}
}
// 에러 Argument of type '(this: Cat) => void' is not assignable to parameter of type '(this: void, ...params: any[]) => void'.
new EventBus().on('click', new Cat('Neko').meow);
([this 의 타입](/articles/함수-typescript 노트 5/#articleHeader8) 에서 발췌)
context 타입을 추적
this 타입이 있으면, bind, call, apply 등의 시나리오에서도 올바르게 타입 제약을 유지할 수 있으며, 현재의 함수 this 와 건네진 타겟 오브젝트 타입이 일치하는 것을 요구합니다:
apply<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args: A): R;
call<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R;
bind<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T): (...args: A) => R;
유사한 에러를 노출시킵니다 (strictBindCallApply 옵션을 온할 필요가 있습니다):
class C {
constructor(a: number, b: string) {}
foo(this: this, a: number, b: string): string { return "" }
}
declare let c: C;
let f14 = c.foo.bind(undefined); // Error
let c14 = c.foo.call(undefined, 10, "hello"); // Error
let a14 = c.foo.apply(undefined, [10, "hello"]); // Error
P.S. bind, call, apply 등의 타입 제약의 더 많은 정보는, Strict bind, call, and apply methods on functions 참조
아직 댓글이 없습니다