This types: tipando o contexto de execução

1. Entendendo o this em JavaScript e os desafios de tipo

O this em JavaScript é uma das fontes mais frequentes de bugs e confusão. Diferente de linguagens como Java ou C#, onde this sempre se refere à instância atual da classe, em JavaScript o valor de this é determinado dinamicamente no momento da chamada da função.

function mostrarThis() {
  console.log(this);
}

const obj = { nome: "Objeto", mostrarThis };

// Chamada direta: this = global (ou undefined em strict mode)
mostrarThis(); // undefined (strict mode)

// Chamada como método: this = obj
obj.mostrarThis(); // { nome: "Objeto", mostrarThis: [Function] }

// Perda de contexto comum em callbacks
const handler = obj.mostrarThis;
handler(); // undefined (this perdido!)

Arrow functions, por outro lado, capturam o this do escopo léxico:

const obj2 = {
  nome: "Objeto 2",
  mostrarThis: () => {
    console.log(this); // this do escopo externo, não do obj2
  }
};

obj2.mostrarThis(); // provavelmente window/global, não obj2

TypeScript não pode inferir automaticamente o contexto do this em funções soltas ou callbacks. Para resolver isso, a linguagem oferece mecanismos de tipagem explícita do contexto.

2. Declarando o tipo do this em funções e métodos

TypeScript permite declarar o tipo esperado para this usando um parâmetro fictício especial. Esse parâmetro deve ser o primeiro na assinatura da função:

interface Contexto {
  nome: string;
  versao: number;
}

function exibirContexto(this: Contexto) {
  console.log(`Executando em: ${this.nome}, versão ${this.versao}`);
}

const ctx: Contexto = { nome: "App", versao: 2 };
exibirContexto.call(ctx); // OK

// Erro de tipo se chamar sem contexto correto
// exibirContexto(); // Erro: O 'this' de tipo 'void' não pode ser atribuído a 'Contexto'

Em métodos de objeto, a tipagem explícita do this é especialmente útil para garantir que o método seja chamado no contexto correto:

class Contador {
  private valor = 0;

  incrementar(this: Contador) {
    this.valor++;
  }

  obterValor() {
    return this.valor;
  }
}

const contador = new Contador();
const incrementarDesvinculado = contador.incrementar;

// Erro: 'this' não é do tipo 'Contador'
// incrementarDesvinculado();

3. ThisType<T>: moldando o contexto em objetos literais

O tipo utilitário ThisType<T> permite declarar o tipo do this dentro de objetos literais, sem precisar criar classes. Isso é extremamente útil em mixins e configurações:

type Metodos = {
  saudacao(this: { nome: string }): void;
};

const pessoa = {
  nome: "Alice",
  saudacao(this: { nome: string }) {
    console.log(`Olá, meu nome é ${this.nome}`);
  }
};

// Com ThisType, podemos criar objetos complexos
interface Configuracao {
  nome: string;
  metodos: ThisType<{ nome: string }> & {
    saudar(): void;
  };
}

function criarConfiguracao(config: Configuracao) {
  return config;
}

const config = criarConfiguracao({
  nome: "Sistema",
  metodos: {
    saudar() {
      // this tem tipo { nome: string } graças a ThisType
      console.log(`Configuração: ${this.nome}`);
    }
  }
});

4. this em callbacks e funções de alta ordem

Ao tipar callbacks que serão passados para APIs como addEventListener, é crucial especificar o this esperado:

interface ElementoDOM {
  addEventListener(
    tipo: string,
    handler: (this: ElementoDOM, evento: Event) => void
  ): void;
}

const elemento: ElementoDOM = {
  addEventListener(tipo, handler) {
    // Simulação
  }
};

elemento.addEventListener("click", function(this: ElementoDOM, evento) {
  console.log(`Clicado em elemento:`, this);
  // this é tipado como ElementoDOM
});

// Arrow functions não podem ter this tipado
// elemento.addEventListener("click", (this: ElementoDOM, evento) => {}); // Erro!

Para funções genéricas que aceitam callbacks com contexto específico:

function executarComContexto<T>(
  this: T,
  fn: (this: T, ...args: any[]) => void
) {
  fn.call(this);
}

A diferença entre this: void e omitir o parâmetro:

// this: void - a função não espera um contexto
function semContexto(this: void, x: number) {
  // não usa this
}

// Omitir this - TypeScript pode inferir como any ou o contexto do escopo
function semDeclaracao(x: number) {
  console.log(this); // this: any (ou contexto do chamador)
}

5. Contexto em classes e herança com this polimórfico

O tipo this polimórfico permite que métodos retornem o tipo da subclasse, possibilitando encadeamento seguro:

class Base {
  constructor(protected valor: number) {}

  definir(this: this, valor: number): this {
    this.valor = valor;
    return this;
  }

  obter(): number {
    return this.valor;
  }
}

class Derivada extends Base {
  constructor(valor: number, protected extra: string) {
    super(valor);
  }

  definirExtra(extra: string): this {
    this.extra = extra;
    return this;
  }
}

const obj = new Derivada(10, "teste");
const resultado = obj.definir(20).definirExtra("novo");
// resultado é do tipo Derivada, não Base

Limitações: o polimorfismo do this não funciona para propriedades:

class Base2 {
  valor: this = this; // Erro! Não pode usar 'this' como tipo de propriedade
}

6. ThisParameterType e OmitThisParameter: manipulando o contexto

ThisParameterType extrai o tipo do parâmetro this de uma função:

function funcaoComThis(this: { id: number }, x: string) {
  return x;
}

type TipoThis = ThisParameterType<typeof funcaoComThis>;
// TipoThis = { id: number }

OmitThisParameter remove o parâmetro this da assinatura, gerando uma função sem contexto:

type FuncaoSemThis = OmitThisParameter<typeof funcaoComThis>;
// (x: string) => string

const fnAdaptada: FuncaoSemThis = funcaoComThis.bind({ id: 1 });
// Agora pode ser chamada sem se preocupar com this

Aplicação prática: adaptar funções com this tipado para contextos genéricos:

interface EventHandler<T> {
  (this: T, evento: Event): void;
}

function adaptarHandler<T>(
  handler: EventHandler<T>
): OmitThisParameter<EventHandler<T>> {
  return handler.bind(null as any) as any;
}

7. Boas práticas e armadilhas comuns

Quando evitar tipagem explícita do this:
- Em arrow functions (não suportam parâmetro this)
- Em funções que não usam this (prefira this: void)
- Em métodos de classe padrão (TypeScript já infere corretamente)

Erros frequentes:
- Esquecer o parâmetro this em callbacks de bibliotecas que exigem contexto
- Usar this tipado em arrow functions (resulta em erro de compilação)
- Confundir this: void com ausência de declaração

Dicas de depuração:
- Ative --noImplicitThis no tsconfig.json para forçar declaração explícita
- Use o TypeScript Playground para testar comportamentos do this
- Verifique se o contexto está correto com this.call ou this.apply

// Configuração recomendada no tsconfig.json
// {
//   "compilerOptions": {
//     "noImplicitThis": true,
//     "strict": true
//   }
// }

A tipagem do this em TypeScript é uma ferramenta poderosa para evitar bugs de contexto em tempo de compilação. Dominá-la é essencial para escrever código TypeScript seguro e previsível, especialmente em aplicações complexas com callbacks, eventos e herança.

Referências