Classes
TypeScript fornece recursos completos do paradigma de Programação Orientada a Objetos. Um destes recursos é a utilização de classes, para detinição de tipos de dados, bem como a utilização de herança.
Exemplo:
class Saudacao {
mensagem: string;
constructor(mensagem: string) {
this.mensagem = mensagem;
}
saudar(pessoa: string): string {
let m: string = this.mensagem;
m = m.replace('{pessoa}', pessoa);
return m;
}
}
let s:Saudacao = new Saudacao('Olá, {pessoa}!'); // instância de Saudacao
s.saudar('José'); // retorna 'Olá, José!'
A sintaxe é similar à de outras linguagens orientadas a objeto, como C# e Java:
- A palavra
class
é utilizada para declarar a classe. - A declaração de atributos da classe precede o construtor
- O construtor da classe é representado pela função
constructor()
- Membros da classe são acessados por meio da palavra
this
- Outras funções da classe são declaradas após o construtor
- A palavra
new
é utilizada para se criar uma instância da classe
Herança
Um dos padrões de projeto mais comuns em programação orientada a objeto é a utilização de herança, permitindo a extensão de tipos para criar novos. TypeScript também fornece esse recurso. O diagrama de classes a seguir apresenta a relação entre quatro classes: Animal, Cavalo, Cobra e Ponto.
A classe Animal
usa a classe Ponto
(por causa do atributo posicao
). As classes Cavalo
e Cobra
herdam (ou estendem) a classe Animal
. O código a seguir representa esse domínio em TypeScript.
class Ponto {
constructor(public x: number = 0, public y: number = 0) {
}
}
class Animal {
nome: string;
posicao: Ponto;
constructor(nome: string) {
this.nome = nome;
this.posicao = new Ponto();
}
mover(metros: number = 0): void {
this.posicao.x += metros;
}
}
class Cobra extends Animal {
constructor(nome: string) {
super(nome);
}
mover(metros = 5): void {
console.log("Deslizando...");
super.mover(metros);
}
}
class Cavalo extends Animal {
constructor(nome: string) {
super(nome);
}
mover(metros = 45): void {
console.log("Galopando...");
super.mover(metros);
}
}
let nagini = new Cobra("Nagini");
let ajax: Animal = new Cavalo("Ajax");
nagini.mover();
ajax.mover(50);
A palavra extends
é utiliza para criar subclasses Cavalo
e Cobra
, que herdam de Animal
.
A palavra super
é utilizada para acessar um membro da superclasse. Por exemplo, no construtor da classe Cobra
há uma chamada ao construtor da classe Animal
por meio de super()
.
Importante destacar a diferença entre this
e super
. Enquanto this
é usado para referência à classe em questão, super
é utilizado para referência à superclasse. A diferença pode ser sutil (já que o mecanismo de herança realmente torna parte da subclasse tudo o que está definido na superclasse como public
ou protected
) e é fundamental para o recurso de sobrescrita de métodos.
O recurso de sobrescrita de métodos nas subclasses permite que uma subclasse modifique comportamento de um método herdado da superclasse. A classe Cobra
, por exemplo, sobrescreve o método mover()
da classe Animal
adicionando funcionalidade e pode chamar o método de mesmo nome da superclasse por meio de super.mover()
.
Um comportamento bastante interessante do código é que a variável ajax
é declarada como sendo do tipo Animal
, mas, por ser uma instância de Cavalo
, é o método mover()
de Cavalo
que é chamado em tempo de execução, não de Animal
-- obviamente, como já foi dito, o método na sublcasse chama o da superclasse por meio de super
.
Modificadores public
, private
e protected
public
por padrão
Em algumas linguagens de programação orientadas a objetos o padrão para declaração de membros da classe é que sejam private
. No caso do TypeScript, o padrão é que os membros são public
.
class Animal {`
public nome: string;
constructor(nome: string) {
this.nome = nome;
}
}
let garfield = new Animal('garfield');
console.log(garfield.nome); // imprime 'garfield'
O modificador public
permite que o membro da classe seja acessível de fora dela.
Por ser o modificador padrão, ele não precisa ser utilizado explicitamente.
Modificador private
Um membro marcado como private
só pode ser acessado dentro da própria classe.
class Animal {
private nome: string;
construtor(nome: string) {
this.nome = nome;
}
}
let garfield = new Animal('garfield');
console.log(garfield.nome); // ERRO! membro privado inacessível
Uma característica importante do TypeScript ao tratar o modificador private
precisa ser destacada. No momento de verificar a equivalência entre tipos, o TypeScript considera os atributos que não estejam marcados como private
. Exemplo:
class Animal {
private nome: string;
constructor(nome: string) {
this.nome = nome;
}
}
class Cobra extends Animal {
constructor(nome: string) {
super(nome);
console.log(this.nome); // ERRO! atributo privado na superclasse
console.log(super.nome); // ERRO! atributo privado na superclasse
}
}
class Pessoa {
private nome: string;
constructor(nome: string) {
this.nome = nome;
}
}
let animal = new Animal('gato');
let cobra = new Cobra('cobra');
let pessoa = new Pessoa('pessoa');
animal = cobra; // OK! estrutura de tipos equivalente
animal = pessoa; // ERRO! estrutura de tipos não equivalente
O código anterior declara as classes Animal
, Cobra
(subclasse de Animal
) e Pessoa
. A classe Animal
possui o atributo nome
, que está marcado como private
, o que também acontece de forma similar com a classe Pessoa
.
O primeiro erro é tentar acessar o atributo nome
na classe Cobra
. Como ele foi declarado como private
, somente a classe em que foi declarado possui acesso a ele -- não importa se acessando como this
ou super
.
O outro erro é menos claro e depende do entendimento do mecanismo de equivalência de tipos (ou estrutura) do TypeScript. Antes, na verdade, é importante lembrar do mecanismo de tipos. Uma variável declarada sem um tipo explícito continua tendo um tipo (mesmo que undefined
, se não tiver sido iniciada). Isso quer dizer que as variáveis no código têm seus tipos:
animal
é do tipoAnimal
cobra
é do tipoCobra
pessoa
é dod tipoPessoa
A atribuição animal = cobra
funciona porque as estruturas (os tipos) destas duas variáveis são equivalentes (neste caso, por causa do mecanismo de herança).
A atribuição animal = pessoa
não funciona porque as suas estruturas não são equivalentes. Embora ambas possuam um atributo nome: string
, o fato de serem marcados como private
implica na conclusão de que são, estruturalmente, diferentes. Por isso, o código falha nesse ponto.
Modificador protected
Membroos de classe delarados como protected
podem ser acessados apenas pela classe e por suas subclasses, continuam inacessíveis de fora da classe.
class Animal {
protected nome: string;
constructor(nome: string) {
this.nome = nome;
}
}
class Cobra extends Animal {
constructor(nome: string) {
super(nome);
console.log(this.nome); // ok
}
}
let gato = new Animal('gato');
console.log(gato.nome); // ERRO! atributo não acessível
Popriedades parâmetros
O TypeScript fornece o recurso de propriedades parâmetros, que fornecem um atalho para a declaração de atributos.
class Animal {
constructor(public nome: string) {
}
}
let animal = new Animal('gato');
console.log(animal.nome);
A diferença é que os parêmtros do construtor podem vir acompanhados de um modificador de acesso. No caso do código anterior, o parâmetro nome
é marcado com public
, o que torna este parâmetro um atributo da classe. Assim, pode-se acessá-lo posteriormente e não é necessário atribuir valores de parâmetros do construtor para atributos da classe.
Acessores (get
e set
)
TypeScript fornece o recurso de "acessores", também conhecido como get/set.
class Animal {
constructor(private nome?: string) { }
get Nome(): string {
return this.nome;
}
set Nome(nome: string) {
if (nome.length > 5) {
this.nome = nome;
} else {
console.log('Valor inválido para o atributo `nome`');
}
}
}
let animal = new Animal();
animal.Nome = 'gato';
O acessor get
permite retornar o valor de um atributo marcado como private
, enquanto o set
permite definir o valor. A maior utilidade de acessores é na garantia de certas regras de negócio. No exemplo anterior, a regra é que a string
a ser atribuída para o atributo nome
precisa ter mais de cinco caracteres.
Membros static
O recurso de membros static
permite acessar conteúdo de uma classe diretamente, sem a necessidade de uma instância.
class Animal {
private static instancias: Animal[] = [];
constructor(public nome?: string) {
Animal.instancias.push(this);
}
static get Instancias(): Animal[] {
return Animal.instancias;
}
}
let gato = new Animal('gato');
let cachorro = new Animal('cachorro');
console.log(Animal.Instancias);
Algumas linguagens de programação fornecem a palavra self
, que dá acesso a membros estáticos. No caso do TypeScript, usa-se diretamente o nome da classe. No código anterior, há o atributo instancias
, marcado como private
e static
. Dentro da classe, o atributo deve ser acessado como Animal.instancias
, ou seja, usa-se o nome da própria classe (ao invés de self
).
O segundo membro marcado como static
é o acessor get
. Nesse caso, a classe fornece uma espécie de acesso apenas de leitura para o atributo instancias
, já que não disponibiliza o acessor set
.
Classes abstratas
O recurso de classes abstratas permite definir a regra de uma classe não poder ser instanciada. Além disso, permite que métodos sejam definidos sem código, situação na qual subclasses são origadas a fornecer implementações para tais métodos.
abstract class Animal {
abstract emitirSom(): void;
mover(metros: number = 5): void {
console.log(`Movendo-se por ${metros}m`);
}
}
class Cobra extends Animal {
emitirSom(): void {
console.log('Barulho de cobra...');
}
}
let cobra = new Cobra();
cobra.emitirSom();
cobra.mover();
cobra.emitirSom();
let gato = new Animal(); // ERRO! não pode instanciar classe abstrata
A palavra abstract
é utilizada para marcar a classe como abstrata e um método como não contendo código. No código anterior, a classe Animal
é abstrata e contém o método emitirSom()
, que também é abstrato. Como regra, se a classe possui um método abstrato, ela também precisa ser abstrata.