Uma classe TypeScript é uma classe JavaScript com tipos: cada campo é declarado com o tipo dele no corpo da classe, e os métodos e o construtor recebem parâmetros e valores de retorno tipados.
A saída compilada é uma classe JavaScript comum sem os tipos, então tudo o que vale para classes em JavaScript continua valendo. O que o TypeScript acrescenta acontece em tempo de compilação: a última chamada é rejeitada antes de o programa rodar.
Declarando campos
Um campo precisa de uma declaração no corpo da classe antes que o construtor possa atribuí-lo. Atribuir this.owner sem declarar owner gera o erro TS2339 (a propriedade não existe). Um campo com inicializador tem o tipo inferido a partir do valor, como uma variável.
| Declaração | Significado |
|---|---|
name: string; | Precisa ser atribuído no construtor |
count = 0; | Inicializado, tipo inferido como number |
label?: string; | Opcional, o tipo é string | undefined |
readonly id: number; | Atribuído uma vez, depois só leitura |
data!: string[]; | Definite assignment: pode confiar, ele vai ser definido |
static total = 0; | Pertence à classe, não às instâncias |
Com strict ligado, o strictPropertyInitialization verifica que todo campo não opcional recebe um valor. Esta classe esquece dois:
O compilador aponta os dois:
index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.
A verificação existe porque, sem ela, new Profile().name.toUpperCase() passaria na verificação de tipos e quebraria com undefined. Corrija cada campo de um de quatro jeitos: dê a ele um inicializador, atribua-o no construtor, marque-o como opcional com ?, ou escreva name!: string quando um framework ou um método init() o define depois. A última opção desliga a verificação para aquele campo, então use com moderação.
Métodos e this
Métodos são tipados como funções. Dentro de um método, o this tem o tipo da instância. Um método pode retornar this para permitir encadeamento, e o tipo de retorno acompanha as subclasses automaticamente.
class QueryBuilder {
private parts: string[] = [];
where(condition: string): this {
this.parts.push(condition);
return this;
}
build(): string {
return this.parts.join(" AND ");
}
}
const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1
Um método perde o this quando você o tira do objeto, um bug clássico do JavaScript. O TypeScript não pega isso por padrão, mas pega se o método declarar um parâmetro this. Esse parâmetro é apagado da saída e só protege o local da chamada:
O campo arrow function funciona porque cada instância recebe a própria função, que captura o this. O custo é uma função por instância, em vez de um único método compartilhado no protótipo.
Getters e setters
Os acessores get e set parecem propriedades vistos de fora. Um getter sem setter é só leitura: atribuir a ele é erro de compilação (TS2540).
O compilador rejeita a atribuição e, se a verificação for contornada, o runtime também lança um TypeError, porque o arquivo compilado roda em strict mode (o compilador gera "use strict" no topo). O RangeError no setter é uma verificação de tempo de execução que você mesmo escreveu. O tipo number só diz que o valor é um número; ele não sabe nada de intervalos.
Membros estáticos
Campos e métodos static pertencem à própria classe. Você os acessa pelo nome da classe, não por uma instância. Um bloco static { } roda uma vez, quando a classe é definida.
Membros estáticos não podem usar os type parameters da classe: em class Box<T>, um static empty: T gera o erro TS2302, porque existe um único campo estático, compartilhado por todo Box<string>, Box<number> e assim por diante.
Implementando uma interface
implements pede ao compilador para verificar que a classe tem tudo o que uma interface exige. Um membro faltando gera o erro TS2420: Class 'X' incorrectly implements interface 'Y'.
Uma classe pode implementar várias interfaces: class Doc implements Printable, Serializable. Há duas coisas que o implements não faz. Ele não tipa os parâmetros dos métodos da classe para você: check(s) { ... } dentro de uma classe que implementa check(s: string): boolean continua gerando o erro TS7006 (Parameter 's' implicitly has an 'any' type), então anote-os. E ele não deixa rastro em tempo de execução, então obj instanceof Shape não é possível.
O private radius no construtor é uma parameter property: declara e atribui o campo em um passo só. Veja construtores.
Uma classe também é um tipo
Uma declaração de classe cria duas coisas com um nome só: um valor (a função construtora que você chama com new) e um tipo (o formato de uma instância). Como o TypeScript compara tipos pela estrutura, qualquer objeto com os mesmos membros públicos se encaixa no tipo, mesmo que não tenha sido criado pela classe.
O objeto simples passa na verificação de tipos, mas não é um Point em tempo de execução: ({ x: 6, y: 8 }) instanceof Point é false. Uma classe com um membro private ou #private impede isso: só instâncias daquela classe (ou das subclasses) podem ser atribuídas a ela.
Erros comuns
- Declarar um campo e nunca atribuí-lo. O
TS2564está dizendo que o campo seriaundefined. Inicialize-o em vez de silenciar com!. - Passar um método como callback.
button.onclick = obj.handleperde othis. Use um campo arrow function ouobj.handle.bind(obj). - Esperar que o
implementsacrescente código ou tipos. Ele só verifica. Os tipos dos parâmetros continuam precisando de anotação. - Usar
instanceofcom uma interface. Interfaces não existem em tempo de execução; verifique uma classe ou use um type guard. - Achar que os tipos validam dados. Um campo
numberaceita qualquer número em tempo de compilação, e qualquer coisa vinda de dados sem tipo em tempo de execução.
Perguntas frequentes
Como criar uma classe no TypeScript?
Declare cada campo com o tipo dele no corpo da classe e depois escreva o construtor e os métodos como em JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. Crie uma instância com new User("Ada").
O que significa "has no initializer and is not definitely assigned in the constructor"?
É o erro TS2564 do strictPropertyInitialization (parte do strict). Um campo tipado como string começaria como undefined porque nada o define. Corrija com um inicializador (name = ""), uma atribuição no construtor, um campo opcional (name?: string) ou, quando algo fora do construtor o define, uma definite assignment assertion (name!: string).
Qual é a diferença entre implements e extends no TypeScript?
extends herda código de uma classe pai: os campos e métodos dela existem na filha em tempo de execução. implements só pede ao compilador para verificar que a classe tem o formato de uma interface. Ele não acrescenta nada à classe e desaparece do JavaScript compilado.
Uma classe pode ser usada como tipo no TypeScript?
Sim. O nome de uma classe é ao mesmo tempo um valor (o construtor) e um tipo (o formato das instâncias). let u: User aceita qualquer objeto com os mesmos membros públicos, porque o TypeScript compara tipos pela estrutura. typeof User é o tipo do próprio construtor.
As classes do TypeScript são diferentes das classes do JavaScript em tempo de execução?
Não. O TypeScript compila uma classe para uma classe JavaScript normal. Anotações de tipo, cláusulas implements e modificadores como private são apagados; só recursos do JavaScript como campos #private, blocos static e getters existem em tempo de execução.