TypeScript позволяет явно указать тип this в функции. Это фиктивный параметр — он не попадает в скомпилированный JS, но даёт TypeScript информацию о контексте вызова.
interface User {
name: string
greet(this: User): string
}
function greet(this: User): string {
return `Привет, я ${this.name}`
}
const user: User = { name: 'Алексей', greet }
user.greet() // OK — this будет User
// Ошибка TS: Cannot assign 'greet' to standalone function
// without ensuring 'this' is User
const fn = user.greet
// fn() // Ошибка TS: void context is not compatible with UserВозвращаемый тип this позволяет строить цепочки методов с правильной типизацией даже в подклассах:
class Builder {
protected config: Record<string, any> = {}
set(key: string, value: any): this { // возвращает this — важно!
this.config[key] = value
return this
}
build(): Record<string, any> {
return { ...this.config }
}
}
class QueryBuilder extends Builder {
table(name: string): this {
return this.set('table', name)
}
limit(n: number): this {
return this.set('limit', n)
}
}
const query = new QueryBuilder()
.table('users') // возвращает QueryBuilder, не Builder!
.set('order', 'name')
.limit(10)
.build()Стрелочные функции захватывают this из окружающего контекста:
class Timer {
private seconds = 0
// Обычный метод — this зависит от вызова
startRegular() {
setInterval(function() {
// this.seconds++ // Ошибка! this — не Timer в callback
}, 1000)
}
// Стрелочная функция — this всегда Timer
startArrow() {
setInterval(() => {
this.seconds++ // OK! this захвачен из startArrow
console.log(this.seconds)
}, 1000)
}
// Метод-стрелка в поле класса — всегда сохраняет this
handleClick = () => {
console.log(this.seconds) // OK при любом вызове
}
}ThisType<T> позволяет типизировать this в объектных методах без классов:
interface AppState {
count: number
text: string
}
interface AppMethods {
increment(): void
setText(text: string): void
reset(): void
}
// ThisType<AppState & AppMethods> — говорит TS что this внутри — это AppState + AppMethods
const methods: AppMethods & ThisType<AppState & AppMethods> = {
increment() { this.count++ }, // this.count типизирован
setText(text) { this.text = text }, // this.text типизирован
reset() { this.count = 0; this.text = '' },
}
function createApp(state: AppState, methods: AppMethods & ThisType<AppState & AppMethods>) {
return Object.assign(state, methods)
}
const app = createApp({ count: 0, text: '' }, methods)
app.increment()
app.setText('Привет')
console.log(app.count, app.text) // 1, 'Привет'class EventHandler {
message = 'Привет!'
// Обычный метод — теряет this при передаче как колбэк
handleClick() {
console.log(this.message) // this может быть undefined!
}
// Метод-стрелка — this всегда EventHandler
handleClickBound = () => {
console.log(this.message) // OK
}
}
const handler = new EventHandler()
const btn = { onclick: null }
btn.onclick = handler.handleClick // опасно — теряем this
btn.onclick = handler.handleClickBound // безопасно
btn.onclick = handler.handleClick.bind(handler) // тоже безопасноMethod chaining с правильным this: построитель SQL-запросов и конфигуратор
// TypeScript: методы возвращают this для поддержки цепочек даже в подклассах
// В JS this работает так же — нужно только правильно возвращать this
class QueryBuilder {
#parts = {
table: null,
conditions: [],
columns: ['*'],
orderBy: null,
limitVal: null,
offsetVal: null,
}
from(table) {
this.#parts.table = table
return this // возвращаем this для chaining
}
select(...columns) {
this.#parts.columns = columns
return this
}
where(condition) {
this.#parts.conditions.push(condition)
return this
}
orderBy(column, direction = 'ASC') {
this.#parts.orderBy = `${column} ${direction}`
return this
}
limit(n) {
this.#parts.limitVal = n
return this
}
offset(n) {
this.#parts.offsetVal = n
return this
}
build() {
if (!this.#parts.table) throw new Error('Таблица не указана')
let sql = `SELECT ${this.#parts.columns.join(', ')} FROM ${this.#parts.table}`
if (this.#parts.conditions.length > 0) {
sql += ` WHERE ${this.#parts.conditions.join(' AND ')}`
}
if (this.#parts.orderBy) sql += ` ORDER BY ${this.#parts.orderBy}`
if (this.#parts.limitVal != null) sql += ` LIMIT ${this.#parts.limitVal}`
if (this.#parts.offsetVal != null) sql += ` OFFSET ${this.#parts.offsetVal}`
return sql
}
}
// Расширяем — this в методах родителя будет PaginatedQueryBuilder!
class PaginatedQueryBuilder extends QueryBuilder {
page(pageNum, pageSize = 20) {
this.limit(pageSize)
this.offset((pageNum - 1) * pageSize)
return this // this — PaginatedQueryBuilder
}
}
// Демонстрация потери this и решения
class Timer {
#seconds = 0
#intervalId = null
// Обычный метод — this зависит от контекста вызова
tick() {
this.#seconds++
console.log(`Тик: ${this.#seconds}`)
}
// Метод-стрелка (поле класса) — this всегда привязан
tickBound = () => {
this.#seconds++
console.log(`Тик (bound): ${this.#seconds}`)
}
start() {
// tick() потеряет this без bind:
// this.#intervalId = setInterval(this.tick, 1000) // НЕПРАВИЛЬНО
// Правильный вариант 1: .bind(this)
// this.#intervalId = setInterval(this.tick.bind(this), 100)
// Правильный вариант 2: стрелочная функция
// this.#intervalId = setInterval(() => this.tick(), 100)
// Правильный вариант 3: метод-стрелка (tickBound)
this.#intervalId = setInterval(this.tickBound, 100)
return this
}
stop() {
clearInterval(this.#intervalId)
console.log(`Остановлен на: ${this.#seconds}`)
return this
}
get seconds() { return this.#seconds }
}
// --- Демонстрация QueryBuilder ---
console.log('=== QueryBuilder (method chaining) ===')
const q1 = new QueryBuilder()
.from('users')
.select('id', 'name', 'email')
.where('active = true')
.where('age > 18')
.orderBy('name')
.limit(10)
.build()
console.log(q1)
// PaginatedQueryBuilder — наследует все методы, this правильный
const q2 = new PaginatedQueryBuilder()
.from('products')
.where('price > 1000')
.orderBy('price', 'DESC')
.page(2, 5) // страница 2, 5 элементов
.build()
console.log(q2)
// Потеря this
console.log('\n=== Потеря this ===')
const timer = new Timer()
const extracted = timer.tick // обычный метод извлечён
try {
extracted() // this = undefined
} catch (e) {
console.log('Ошибка (потеря this):', e.message || e.constructor.name)
}
// Правильно: bind
const bound = timer.tick.bind(timer)
bound() // this = timer, работает
// Метод-стрелка не теряет this
const arrow = timer.tickBound
arrow() // this = timer, всегда работаетTypeScript позволяет явно указать тип this в функции. Это фиктивный параметр — он не попадает в скомпилированный JS, но даёт TypeScript информацию о контексте вызова.
interface User {
name: string
greet(this: User): string
}
function greet(this: User): string {
return `Привет, я ${this.name}`
}
const user: User = { name: 'Алексей', greet }
user.greet() // OK — this будет User
// Ошибка TS: Cannot assign 'greet' to standalone function
// without ensuring 'this' is User
const fn = user.greet
// fn() // Ошибка TS: void context is not compatible with UserВозвращаемый тип this позволяет строить цепочки методов с правильной типизацией даже в подклассах:
class Builder {
protected config: Record<string, any> = {}
set(key: string, value: any): this { // возвращает this — важно!
this.config[key] = value
return this
}
build(): Record<string, any> {
return { ...this.config }
}
}
class QueryBuilder extends Builder {
table(name: string): this {
return this.set('table', name)
}
limit(n: number): this {
return this.set('limit', n)
}
}
const query = new QueryBuilder()
.table('users') // возвращает QueryBuilder, не Builder!
.set('order', 'name')
.limit(10)
.build()Стрелочные функции захватывают this из окружающего контекста:
class Timer {
private seconds = 0
// Обычный метод — this зависит от вызова
startRegular() {
setInterval(function() {
// this.seconds++ // Ошибка! this — не Timer в callback
}, 1000)
}
// Стрелочная функция — this всегда Timer
startArrow() {
setInterval(() => {
this.seconds++ // OK! this захвачен из startArrow
console.log(this.seconds)
}, 1000)
}
// Метод-стрелка в поле класса — всегда сохраняет this
handleClick = () => {
console.log(this.seconds) // OK при любом вызове
}
}ThisType<T> позволяет типизировать this в объектных методах без классов:
interface AppState {
count: number
text: string
}
interface AppMethods {
increment(): void
setText(text: string): void
reset(): void
}
// ThisType<AppState & AppMethods> — говорит TS что this внутри — это AppState + AppMethods
const methods: AppMethods & ThisType<AppState & AppMethods> = {
increment() { this.count++ }, // this.count типизирован
setText(text) { this.text = text }, // this.text типизирован
reset() { this.count = 0; this.text = '' },
}
function createApp(state: AppState, methods: AppMethods & ThisType<AppState & AppMethods>) {
return Object.assign(state, methods)
}
const app = createApp({ count: 0, text: '' }, methods)
app.increment()
app.setText('Привет')
console.log(app.count, app.text) // 1, 'Привет'class EventHandler {
message = 'Привет!'
// Обычный метод — теряет this при передаче как колбэк
handleClick() {
console.log(this.message) // this может быть undefined!
}
// Метод-стрелка — this всегда EventHandler
handleClickBound = () => {
console.log(this.message) // OK
}
}
const handler = new EventHandler()
const btn = { onclick: null }
btn.onclick = handler.handleClick // опасно — теряем this
btn.onclick = handler.handleClickBound // безопасно
btn.onclick = handler.handleClick.bind(handler) // тоже безопасноMethod chaining с правильным this: построитель SQL-запросов и конфигуратор
// TypeScript: методы возвращают this для поддержки цепочек даже в подклассах
// В JS this работает так же — нужно только правильно возвращать this
class QueryBuilder {
#parts = {
table: null,
conditions: [],
columns: ['*'],
orderBy: null,
limitVal: null,
offsetVal: null,
}
from(table) {
this.#parts.table = table
return this // возвращаем this для chaining
}
select(...columns) {
this.#parts.columns = columns
return this
}
where(condition) {
this.#parts.conditions.push(condition)
return this
}
orderBy(column, direction = 'ASC') {
this.#parts.orderBy = `${column} ${direction}`
return this
}
limit(n) {
this.#parts.limitVal = n
return this
}
offset(n) {
this.#parts.offsetVal = n
return this
}
build() {
if (!this.#parts.table) throw new Error('Таблица не указана')
let sql = `SELECT ${this.#parts.columns.join(', ')} FROM ${this.#parts.table}`
if (this.#parts.conditions.length > 0) {
sql += ` WHERE ${this.#parts.conditions.join(' AND ')}`
}
if (this.#parts.orderBy) sql += ` ORDER BY ${this.#parts.orderBy}`
if (this.#parts.limitVal != null) sql += ` LIMIT ${this.#parts.limitVal}`
if (this.#parts.offsetVal != null) sql += ` OFFSET ${this.#parts.offsetVal}`
return sql
}
}
// Расширяем — this в методах родителя будет PaginatedQueryBuilder!
class PaginatedQueryBuilder extends QueryBuilder {
page(pageNum, pageSize = 20) {
this.limit(pageSize)
this.offset((pageNum - 1) * pageSize)
return this // this — PaginatedQueryBuilder
}
}
// Демонстрация потери this и решения
class Timer {
#seconds = 0
#intervalId = null
// Обычный метод — this зависит от контекста вызова
tick() {
this.#seconds++
console.log(`Тик: ${this.#seconds}`)
}
// Метод-стрелка (поле класса) — this всегда привязан
tickBound = () => {
this.#seconds++
console.log(`Тик (bound): ${this.#seconds}`)
}
start() {
// tick() потеряет this без bind:
// this.#intervalId = setInterval(this.tick, 1000) // НЕПРАВИЛЬНО
// Правильный вариант 1: .bind(this)
// this.#intervalId = setInterval(this.tick.bind(this), 100)
// Правильный вариант 2: стрелочная функция
// this.#intervalId = setInterval(() => this.tick(), 100)
// Правильный вариант 3: метод-стрелка (tickBound)
this.#intervalId = setInterval(this.tickBound, 100)
return this
}
stop() {
clearInterval(this.#intervalId)
console.log(`Остановлен на: ${this.#seconds}`)
return this
}
get seconds() { return this.#seconds }
}
// --- Демонстрация QueryBuilder ---
console.log('=== QueryBuilder (method chaining) ===')
const q1 = new QueryBuilder()
.from('users')
.select('id', 'name', 'email')
.where('active = true')
.where('age > 18')
.orderBy('name')
.limit(10)
.build()
console.log(q1)
// PaginatedQueryBuilder — наследует все методы, this правильный
const q2 = new PaginatedQueryBuilder()
.from('products')
.where('price > 1000')
.orderBy('price', 'DESC')
.page(2, 5) // страница 2, 5 элементов
.build()
console.log(q2)
// Потеря this
console.log('\n=== Потеря this ===')
const timer = new Timer()
const extracted = timer.tick // обычный метод извлечён
try {
extracted() // this = undefined
} catch (e) {
console.log('Ошибка (потеря this):', e.message || e.constructor.name)
}
// Правильно: bind
const bound = timer.tick.bind(timer)
bound() // this = timer, работает
// Метод-стрелка не теряет this
const arrow = timer.tickBound
arrow() // this = timer, всегда работаетРеализуй класс `EventEmitter` с методами `on(event, handler)`, `off(event, handler)`, `emit(event, ...args)` — все методы возвращают `this` для поддержки цепочек. Также добавь метод `once(event, handler)` — подписывается на событие, которое будет вызвано ровно один раз, после чего обработчик удаляется. Все методы должны корректно возвращать `this` чтобы работал chaining: `emitter.on("a", fn1).on("b", fn2).emit("a")`.
on: if (!this.#handlers.has(event)) this.#handlers.set(event, []); this.#handlers.get(event).push(handler); return this. once: создай const wrapper = (...args) => { handler(...args); this.off(event, wrapper) }; return this.on(event, wrapper). emit: (this.#handlers.get(event) ?? []).forEach(h => h(...args)); return this.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке