Типизация this
Как TypeScript типизирует скрытый первый параметр через `this: T` (стирается при эмите), почему метод теряет свой `this` при передаче как колбэк, и как ThisType, полиморфный `this` и this-гарды моделируют fluent- и stateful-API.
Классовый компонент работает идеально, пока кто-то не пишет button.addEventListener("click", this.handleClick). В рантайме this внутри handleClick теперь — это сам <button>, а не компонент, и чтения this.state оказываются undefined. В чистом JavaScript баг невидим, пока не упадёт. TypeScript может поймать его на этапе компиляции — но только если вы понимаете, что у каждого метода есть скрытый первый параметр с именем this, и что отделение метода срывает эту привязку.
Фейковый первый параметр
Зачем TypeScript вообще нужен особый механизм для this? Потому что this — единственное значение в JavaScript, которое не определяется местом определения функции: оно задаётся заново в каждой точке вызова, и компилятор не может вывести его из исходного положения.
Функции в JavaScript получают неявного получателя: значение this, определяемое тем, как функцию вызвали, а не где её определили. TypeScript позволяет типизировать этого получателя специальным первым параметром с именем this. Он выглядит как параметр, но им не является — он стирается при эмите и никогда не появляется в списке аргументов вызова.
interface Card { suit: string; value: number }
function describe(this: Card): string {
return `${this.value} of ${this.suit}`;
}
const card: Card = { suit: "hearts", value: 10 };
describe.call(card); // ok — `this` подаётся через .call
describe();
// Error: The 'this' context of type 'void' is not assignable
// to method's 'this' of type 'Card'.Сэмиттированный JavaScript — это просто function describe() { return this.value + ...; }: аннотация this: Card исчезла. Её единственная задача была — проверить получателя в каждой точке вызова. При --noImplicitThis (входит в strict) this, использованный внутри функции без аннотации и без выводимого контейнера, помечается как неявный any, заставляя вас его объявить.
Методы несут выведенный this и теряют его при отделении
Внутри метода объекта или метода класса TypeScript выводит this как содержащий объект/экземпляр. Проблема возникает, когда вы отделяете метод — присваиваете его переменной или передаёте как колбэк, — потому что привязку задаёт вызывающий, а не определение.
class Counter {
count = 0;
inc(this: Counter) { // явный this ради безопасности
this.count++;
}
}
const c = new Counter();
c.inc(); // ok — получатель `c`
const f = c.inc; // отделён
f();
// Error: The 'this' context of type 'void' is not assignable
// to method's 'this' of type 'Counter'.Это проблема потерянного this. При проверках strictBindCallApply и метода-this TypeScript помечает несвязанный вызов. Рантайм-фиксы привычные — c.inc.bind(c) или стрелочное поле-функция inc = () => { this.count++ }, чей this лексически является экземпляром, — но именно система типов выводит баг на поверхность до того, как он уедет в прод.
▸Почему это работает
Почему стрелочная функция чинит это, а метод — нет? У метода this динамичен — разрешается из вызова. У стрелочной функции нет своего this; она замыкается над this объемлющей области в момент определения. Поэтому поле класса onClick = () => … лексически захватывает экземпляр, и вызывающий не может его переназначить. Цена — одно замыкание на экземпляр вместо одного общего метода прототипа — обычно пренебрежимо, изредка значимо для объектов, создаваемых в горячих циклах.
This-гарды типов и полиморфный this
Две this-типизированные возможности питают реальные API. Во-первых, метод может вернуть this is Derived — this-гард типа, который сужает самого получателя в вызывающей области:
class FileNode {
isDir(): this is DirNode { // сужает `this`, а не параметр
return "children" in this;
}
}
class DirNode extends FileNode {
children: FileNode[] = [];
}
declare const node: FileNode;
if (node.isDir()) {
node.children;
// ^? FileNode[] — node сужен до DirNode внутри ветки
}Во-вторых, полиморфный this делает тип возврата метода «каким бы ни был текущий класс», что держит fluent-билдеры (API с цепочкой вызовов вида .where(...).byEmail(...)) корректно типизированными при наследовании:
class Query {
where(cond: string): this { // возвращает конкретный подкласс, а не Query
return this;
}
}
class UserQuery extends Query {
byEmail(e: string): this { return this; }
}
new UserQuery().where("active").byEmail("a@b.com");
// ^^^^^^^ ok — where() вернул UserQuery, а не QueryЕсли бы where вернул Query вместо this, цепочка сломалась бы на .byEmail, потому что у Query нет такого метода. Полиморфный this — это механизм на уровне типов за цепочечными билдерами. ThisType<T> — родственный маркер контекстной типизации, который стандартная библиотека использует, чтобы типизировать this внутри объектных литералов с наборами методов (объект options во Vue 2, миксины, fluent-конфиг), так что методы внутри литерала видят весь собранный объект как свой this.
Дано `const f = obj.method; f();`, где `method` типизирован с `this: Obj`. Почему TypeScript помечает вызов `f()`?
`this` метода — как ключ-карта отеля, работающая, лишь пока вы заселены в конкретный номер. Пронеси голую карту в другое здание, и система по аналогии сочтёт её принадлежащей ___ — поэтому отделённый метод теряет своего исходного получателя.
- 01Объясните проблему потерянного `this` от начала до конца: как TypeScript типизирует `this`, почему отделение метода её ломает и два стандартных фикса.
- 02Сравните this-гард типа, полиморфный `this` и ThisType<T> — что моделирует каждый?
TypeScript типизирует получателя на момент вызова фейковым первым параметром this: T, который стирается при эмите и нужен лишь для проверки получателя в каждой точке вызова; --noImplicitThis (в strict) заставляет объявить его, когда вывести нельзя. Методы выводят this как свой контейнер, но привязка динамична, поэтому отделение метода теряет this — баг потерянного this, который TypeScript ловит проверками this-параметра и strictBindCallApply, а чинят его .bind или стрелочное поле класса. Для API this is Derived сужает получателя, полиморфный this типизирует fluent-билдеры при наследовании, а ThisType<T> типизирует this внутри объектных литералов с наборами методов. Дальше мы переходим от типизации получателя к изменению потока управления: функции-утверждения, сигнатуры asserts x is T, сужающие остаток области через выброс исключения. Теперь, когда вы видите краш с this.state is undefined внутри обработчика событий, вы знаете ровно, что произошло — и знаете две строки, которые нужны, чтобы починить это до того, как баг уедет в прод.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.