TypeScript

Compreendendo o TypeScript

Peter Vogel

De muitas formas, é útil pensar no TypeScriptpor seus próprios méritos. A especificação da linguagem TypeScript se refere ao TypeScript como "um açúcar sintético para o JavaScript". É verdade e provavelmente uma etapa essencial na busca por púbico alvo da linguagem - desenvolvedores do lado do cliente atualmente usando o JavaScript.

E você precisa compreender o JavaScript antes de compreender o TypeScript. Na verdade, a especificação da linguagem (você pode ler em bit.ly/1xH1m5B) geralmente descreve as construções TypeScript em termos de código JavaScript resultante. Mas é igualmente útil pensar no TypeScript como uma linguagem própria que compartilha recursos com o JavaScript.

Por exemplo, como o C#, o TypeScript é uma linguagem de dados digitados, que lhe dá suporte IntelliSense e de verificação de tempo de compilação, entre outros recursos. Como o C#, o TypeScript inclui expressões genéricas e lambda (ou seu equivalente).

Mas o TypeScript, claro, não é o C#. Compreender o que é único sobre o TypeScript é tão importante como compreender o que o TypeScript compartilha com a linguagem do lado do servidor que você está usando atualmente. O sistema do tipo TypeScript é diferente (e mais simples) do que o C#. O TypeScript se aproveita de sua compreensão de outros modelos de objeto de uma forma única e executa a herança de forma diferente do que o C#. E por causa do TypeScript compilar para o JavaScript, o TypeScript compartilha muitos de seus fundamentos com o JavaScript, ao contrário do C#.

A questão que permanece: "você prefere escrever o código do lado do cliente nesta linguagem ou em JavaScript?"

TypeScript são dados digitados

O TypeScript não tem muitos tipos de dados integrados que você pode usar para declarar variáveis - apenas cadeias de caracteres, números e Boolean. Esses três tipos são um subtipo do qualquer tipo (que você também pode usar ao declarar as variáveis). Você pode definir ou testar as variáveis declaradas com esses quatro tipos contra os tipos nulos ou indefinidos. Você também pode declarar os métodos como nulos, indicando que eles não retornam um valor.

Esse exemplo declara uma variável como cadeia de caracteres:

var name: string;

Você pode estender este tipo de sistema simples com valores enumerados e quatro espécies de tipos de objetos: interfaces, classes, matrizes e funções. Por exemplo, o código a seguir define uma interface (uma espécie de tipo de objeto) com o nome ICustomerShort. A interface inclui dois membros: uma propriedade chamada Id e um método chamado CalculateDiscount:

interface ICustomerShort
{
  Id: number;
  CalculateDiscount(): number;
}

Como em C#, você pode usar interfaces ao declarar as variáveis e os tipos de retorno. Esse exemplo declara a variável cs como tipo ICustomerShort:

var cs: ICustomerShort;

Você também pode definir tipos de objetos como classes, que, ao contrário de interfaces, podem conter código executável. Este exemplo define uma classe chamada CustomerShort com uma propriedade e um método:

class CustomerShort
{
  FullName: string;
  UpdateStatus( status: string ): string
  {
    ...manipulate status... 
    return status;
  }
}

Como as versões mais recentes de C#, não é necessário fornecer o código de implementação ao definir uma propriedade. A simples declaração do nome e tipo é suficiente. As classes podem implementar uma ou mais interfaces, conforme mostrado na Figura 1, que adiciona a minha interface ICustomerShort, com sua propriedade, à minha classe CustomerShort.

Figura 1 Adicionar uma Interface a uma Classe

class CustomerShort implements ICustomerShort
{
  Id: number;
  FullName: string;
  UpdateStatus(status: string): string
  {
    ...manipulate status...
    return status;
  }
  CalculateDiscount(): number
  {
    var discAmount: number;
    ...calculate discAmount...
    return discAmount;
  }
}

Conforme a Figura 1 mostra, a sintaxe para a implementação de uma interface é tão simples no TypeScript como no C#. Para implementar os membros da interface você simplesmente adiciona membros com o mesmo nome, em vez de juntar o nome da interface aos membros da classe relevante. Neste exemplo, eu simplesmente adicionei o Id e o CalculateDiscount à classe para implementar o ICustomerShort. O TypeScript também permite que você use tipos de objeto literais. Este código define a variável CST para um objeto literal contendo uma propriedade e um método:

var csl = {
            Age: 61,
            HaveBirthday(): number
          {
            return this.Age++;
          }
        };

Este exemplo usa um tipo de objeto para especificar o valor de retorno do método UpdateStatus:

UpdateStatus( status: string ): { 
  status: string; valid: boolean }
{
  return {status: "New",
          valid: true
         };
}

Além de tipos de objetos (classe, interface, literais e matriz), você também pode definir os tipos de funções que descrevem a assinatura de uma função. O código a seguir reescreve o CalculateDiscount da minha classe CustomerShort para aceitar um único parâmetro chamado discountAmount:

interface ICustomerShort
{
  Id: number;
  CalculateDiscount( discountAmount:
    ( discountClass: string, 
      multipleDiscount: boolean ) => number): number
}

Esse parâmetro é definido usando um tipo de função que aceita dois parâmetros (um da cadeia de caracteres, um de boolean) e retorna um número. Se você é um desenvolvedor de C#, você pode achar que a sintaxe se parece muito com uma expressão lambda.

Uma classe que implementa essa interface seria algo parecido com a Figura 2.

Figura 2 Essa classe implementa a interface adequada

class CustomerShort implements ICustomerShort
{
  Id: number;
  FullName: string;
  CalculateDiscount( discountedAmount:
    ( discountClass: string, 
      multipleDiscounts: boolean ) => number ): number
  {
    var discAmount: number;
    ...calculate discAmount...
    return discAmount;
  }
}

Como as versões recentes do C#, o TypeScript também infere o tipo de dados de uma variável do valor para o qual a variável é inicializada. Neste exemplo, o TypeScript assumirá que o myCust variável é do CustomerShort:

var myCust= new CustomerShort();
myCust.FullName = "Peter Vogel";

Como o C#, você pode declarar variáveis usando uma interface e, em seguida, definir a variável como um objeto que implementa essa interface:

var cs: ICustomerShort;
cs = new CustomerShort();
cs.Id = 11;
cs.FullName = "Peter Vogel";

Finalmente, você também pode usar parâmetros de tipo (que se parecem suspeitamente como genéricos no C#) para permitir que o código de chamada especifique o tipo de dado a ser usado. Este exemplo permite que o código que cria a classe defina o tipo de dado da propriedade Id:

class CustomerTyped<T>
{
  Id: T;
}

Este código define o tipo de dado da propriedade Id para uma cadeia de caracteres antes de usá-lo:

var cst: CustomerTyped<string>;
cst = new CustomerTyped<string>();
cst.Id = "A123";

Para isolar as classes, interfaces e outros membros públicos e evitar colisões de nomes, você pode declarar essas construções dentro dos módulos tanto como os namespaces do C#. Você terá que marcar aqueles itens que você deseja disponibilizar para outros módulos com a palavra-chave de exportação. O módulo na Figura 3 exporta duas interfaces e uma classe.

Figura 3 Exportar duas interfaces e uma classe

module TypeScriptSample
{
  export interface ICustomerDTO
  {
    Id: number;
  }
  export interface ICustomerShort extends ICustomerDTO
  {
    FullName: string;
  }
  export class CustomerShort implements ICustomerShort
  {
    Id: number;
    FullName: string;
  }

Para usar os componentes exportados, você pode prefixar o nome do componente com o nome do módulo, como neste exemplo:

var cs: TypeScriptSample.CustomerShort;

Ou você pode usar a palavra-chave de importação do TypeScript de modo a estabelecer um atalho para o módulo:

import tss = TypeScriptSample;
...
var cs:tss.CustomerShort;

O TypeScript é flexível em relação aos dados de digitação

Tudo isso deve ser familiar se você é um programador de C#, exceto talvez, a reversão de declarações de variáveis (nome da variável primeiro, o tipo de dado segundo) e objetos literais. No entanto, praticamente todos os tipos de dados no TypeScript são opcionais. A especificação descreve os tipos de dados como "anotações". Se você omitir tipos de dados (e o TypeScript não infere o tipo de dado), os tipos de dados padrão para qualquer tipo.

O TypeScript não exige correspondência estrita de tipos de dados, também. O TypeScript usa o que a especificação chama de "subtipo estrutural" para determinar a compatibilidade. Isto é semelhante ao que é muitas vezes é chamado de "digitação do tipo pato". No TypeScript, duas classes são consideradas idênticas se elas têm membros com os mesmos tipos. Por exemplo, aqui está uma classe Customer-Short que implementa uma interface chamada ICustomerShort:

interface ICustomerShort
{
  Id: number;
  FullName: string;
}
class CustomerShort implements ICustomerShort
{
  Id: number;
  FullName: string;
}

Aqui está uma classe chamada CustomerDeviant que é parecida com a minha classe CustomerShort:

class CustomerDeviant
{
  Id: number;
  FullName: string;
}

Graças ao subtipo estrutural, posso usar CustomerDevient com variáveis definidas com a minha classe CustomerShort ou interface ICustomerShort. Esses exemplos usam CustomerDeviant alternadamente com as variáveis declaradas como CustomerShort ou ICustomerShort:

var cs: CustomerShort;
cs = new CustomerDeviant
cs.Id = 11;
var csi: ICustomerShort;
csi = new CustomerDeviant
csi.FullName = "Peter Vogel";

Esta flexibilidade permite que você atribua objetos literais do TypeScript para as variáveis declaradas como classes ou interfaces, desde que eles sejam estruturalmente compatívei, conforme eles estejam aqui:

var cs: CustomerShort;
cs = {Id: 2,
      FullName: "Peter Vogel"
     }
var csi: ICustomerShort;
csi = {Id: 2,
       FullName: "Peter Vogel"
      }

Isto leva a recursos específicos do TypeScript em torno de tipos aparentes, supertipos e subtipos que levam à questão geral da atribuição, que eu pularei aqui. Essas características permitiriam o CustomerDeviant, por exemplo, aos membros que não estão presentes no CustomerShort sem fazer com que o meu código de exemplo falhe.

TypeScript tem classe

A especificação doTypeScript se refere à linguagem conforme implementa "o padrão de classe [usando] cadeias de protótipos para implementar muitas variações sobre os mecanismos de herança orientada a objetos". Na prática, isso significa que o TypeScript não é apenas dados digitados, mas efetivamente orientados ao objeto.

Da mesma forma que uma interface C# pode herdar de uma interface de base, uma interface TypeScript pode estender uma outra interface, mesmo se essa outra interface é definida em um módulo diferente. Este exemplo estende a interface ICustomerShort para criar uma nova interface chamada ICustomerLong:

interface ICustomerShort
{
  Id: number;
}
interface ICustomerLong extends ICustomerShort
{
  FullName: string;
}

A interface ICustomerLong terá dois membros: FullName e Id. Na interface mesclada, os membros da interface aparecem primeiro. Portanto, a minha interface ICustomerLong é equivalente a esta interface:

interface ICustomerLongPseudo
{
  FullName: string;
  Id: number;
}

Uma classe que implementa ICustomerLong precisaria de ambas as propriedades:

class CustomerLong implements ICustomerLong
{
  Id: number;
  FullName: string;
}

As classes pode estender outras classes, da mesma forma que uma interface pode estender outra. A classe na Figura 4 estende o CustomerShort e adiciona uma nova propriedade para a definição. Ele usa getters e setters específicos para definir as propriedades (embora não de uma forma particularmente útil).

Figura 4 Propriedades definidas com os Getters e Setters

class CustomerShort
{
  Id: number;
}
class CustomerLong extends CustomerLong
{
  private id: number;
  private fullName: string;
  get Id(): number
  {
    return this.id
  }
  set Id( value: number )
  {
    this.id = value;
  }
  get FullName(): string
  {
    return this.fullName;
  }
  set FullName( value: string )
  {
    this.fullName = value;
  }
}

O TypeScript reforça a prática mais recomendada de acessar campos internos (como o id e o fullName) através de uma referência para a classe (esta). As classes também podem ter funções de construtor, que incluem um recurso de C# adotado recentemente: definição automática de campos. A função de construtor em uma classe do TypeScript deve ser nomeado construtor e seus parâmetros públicos são automaticamente definidos como propriedades e inicializados a partir dos valores passados para eles. Neste exemplo, o construtor aceita um único parâmetro chamado Company do tipo cadeia de caracteres:

export class CustomerShort implements ICustomerShort
{
  constructor(public Company: string)
  {       }

Como o parâmetro Company é definido como público, a classe também recebe uma propriedade pública denominada Company inicializado a partir do valor passado para o construtor. Graças a esse recurso, a comp. variável será definida para "PH&VIS", conforme neste exemplo:

var css: CustomerShort;
css = new CustomerShort( "PH&VIS" );
var comp = css.Company;

Declarar o parâmetro de um construtor como privado cria uma propriedade interna que só pode ser acessada a partir do código dentro dos membros da classe através da palavra-chave deste. Se o parâmetro não é declarado como público ou privado, nenhuma propriedade é gerada.

Sua classe deve ter um construtor. Como em C#, se você não fornecer um, um será fornecido para você. Se sua classe estender outra classe, qualquer construtor que você criar deve incluir uma chamada para super. Isso chama o construtor da classe que ele estende. Este exemplo inclui um construtor com uma super chamada que fornece parâmetros para o construtor da classe de base:

class MyBaseClass
{
  constructor(public x: number, public y: number ) { }   
}
class MyDerivedClass extends MyBaseClass
{
  constructor()
  {
    super(2,1);
  }
}

TypeScript herda de forma diferente

Novamente, tudo isso vai parecer familiar para você, se você é um programador C#, com exceção de algumas palavras-chave engraçadas (estender). Mas, novamente, estender uma classe ou uma interface não é exatamente a mesma coisa que os mecanismos de herança em C#. A especificação TypeScript usa os termos usuais para a classe que está sendo estendida ("classe base") e a classe que a estende ("classe derivada"). No entanto, a especificação refere-se a uma classe de '"especificação da herança", por exemplo, em vez de usar a palavra "herança".

Para começar, o TypeScript tem menos opções do que o C# quando diz respeito com a definição de classes de base. Você não pode declarar a classe ou os membros como não-substituíveis, abstratos ou virtuais (embora interfaces forneçam grande parte da funcionalidade que uma classe de base virtual fornece).

Não há nenhuma maneira de evitar que alguns membros não sejam herdados. Uma classe derivada herda todos os membros da classe base, incluindo os membros públicos e privados (todos os membros públicos da classe base são substituíveis enquanto os membros privados não são). Para substituir um membro público, simplesmente defina um membro na classe derivada com a mesma assinatura. Enquanto você pode usar a palavra-chave super para acessar um método público de uma classe derivada, você não pode acessar uma propriedade na classe base usando super (embora você possa substituir a propriedade).

O TypeScript permite aumentar uma interface simplesmente declarando uma interface com um nome idêntico e novos membros. Isso permite estender o código JavaScript existente sem criar um novo tipo nomeado. O exemplo na Figura 5 define a interface ICustomerMerge através de duas definições de interface separadas e, em seguida, implementa a interface em uma classe.

Figura 5 A interface ICustomerMerge definida através de duas definições de interface

interface ICustomerMerge
{
  MiddleName: string;
}
interface ICustomerMerge
{
  Id: number;
}
class CustomerMerge implements ICustomerMerge
{
  Id: number;
  MiddleName: string;
}

As classes também pode estender a outras classes, mas não interfaces. No TypeScript, as interfaces também pode estender as classes, mas apenas de uma forma que envolva herança. Quando uma interface estende uma classe, a interface inclui todos os membros da classe (públicos e privados), mas sem as implementações da classe. Na Figura 6, a interface ICustomer terá o id do membro privado, Id do membro público e o MiddleName do membro público.

Figura 6 Uma classe estendida com todos os membros

class Customer
{
  private id: number;
  get Id(): number
  {
    return this.id
  }
  set Id( value: number )
  {
    this.id = value;
  }
}
interface ICustomer extends Customer
{
  MiddleName: string;
}

A interface ICustomer tem uma restrição significativa - você só pode usá-la com classes que estendem a mesma classe que a interface estendida (neste caso, essa é a classe Customer). O TypeScript requer que você inclua membros privados na interface a ser herdada da classe que a interface se estende, em vez de ser reimplantado na classe derivada. Uma nova classe que usa a interface ICustomer seria necessária, por exemplo, para fornecer uma implementação para MiddleName (porque é apenas especificada na interface). O desenvolvedor usando ICustomer poderia optar por herdar ou substituir os métodos públicos da classe Customer, mas não seria capaz de substituir o membro id privado.

Este exemplo mostra uma classe (chamada NewCustomer) que implementa a interface ICustomer e estende a classe de cliente conforme necessário. Neste exemplo, o NewCustomer herda a implementação de Id do Customer e fornece uma implementação para MiddleName:

class NewCustomer extends Customer implements ICustomer
{
  MiddleName: string;
}

Esta combinação de interfaces, classes, implementação e extensão fornece uma forma controlada para as classes que você define para estender as classes definidas em outros modelos de objeto (para obter mais detalhes, consulte a seção 7.3 da especificação de linguagem, "Interfaces Extending Classes"). Junto com a capacidade do TypeScript para usar informações sobre outras bibliotecas JavaScript, permite escrever o código TypeScript que funciona com os objetos definidos nessas bibliotecas.

O TypeScript sabe sobre suas bibliotecas

Além de saber sobre as classes e interfaces definidas em seu aplicativo, você pode fornecer o TypeScript com informações sobre outras bibliotecas de objetos. Isso é feito através da palavra-chave de declaração do TypeScript. Isso cria o que a especificação chama de "declarações ambientais". Você talvez nunca possa ter que usar a palavra-chave declare porque você pode encontrar arquivos de definição para a maioria das bibliotecas JavaScript no site do DefinitelyTyped em definitelytyped.org. Através destes arquivos de definição, o TypeScript pode efetivamente "ler a documentação" sobre as bibliotecas com as quais você precisa trabalhar.

"A leitura da documentação", é claro, significa que você obtém suporte IntelliSense digitado dos dados e a verificação do tempo de compilação ao usar os objetos que compõem a biblioteca. Ele também permite que o TypeScript, em determinadas circunstâncias, infira no tipo de uma variável a partir do contexto em que ela é usada. Graças ao arquivo de definição lib.d.ts incluído no TypeScript, o TypeScript assume a âncora variável é do tipo HTMLAnchorElement no código a seguir:

var anchor = document.createElement( "a" );

O arquivo de definição especifica que o resultado retornado pelo método createElement quando o método está passando a cadeia de caracteres "a". Conhecer a âncora é um HTMLAnchorElement, que significa que o TypeScript sabe que a variável da âncora suportará, por exemplo, o método addEventListener.

A inferência do tipo de dados TypeScript também funciona com tipos de parâmetro. Por exemplo, o método addEventListener aceita dois parâmetros. O segundo é uma função em que addEventListener passa um objeto do tipo PointerEvent. O TypeScript sabe disso e suporta o acesso da propriedade cancelBubble da classe PointerEvent dentro da função:

span.addEventListener("pointerenter", function ( e )
{
  e.cancelBubble = true;
}

Da mesma forma que lib.d.ts fornece informações sobre o HTML DOM, os arquivos de definição para outro JavaScript fornece funcionalidade semelhante. Depois de adicionar o arquivo backbone.d.ts ao meu projeto, por exemplo, eu posso declarar uma classe que estende a classe Backbone Model e implementar minha própria interface com um código como este:

class CustomerShort extends bb.Model implements ICustomerShort
{
}

Se você estiver interessado em mais detalhes sobre como usar o TypeScript com Backbone e Knockout, confira minhas colunas do TypeScript prático em bit.ly/1BRh8NJ. No ano novo, eu vou estar olhando para os detalhes do uso do TypeScript com Angular.

Há muito mais para TypeScript do que você vê aqui. O TypeScript versão 1.3 está previsto para incluir tipos de dados da união (para suporte, por exemplo, as funções que retornam uma lista de tipos específicos) e tuplas. A equipe TypeScript está trabalhando com outras equipes que aplicam a digitação de dados para JavaScript (Flow e Angular) para garantir que o TypeScript funcionará com uma gama tão ampla de bibliotecas JavaScript possível.

Se você precisa fazer algo que suporta o JavaScript e o TypeScript não deixa você fazer, você sempre pode integrar o seu código JavaScript, porque o TypeScript é um superconjunto de JavaScript. Portanto, a questão permanece: qual dessas linguagens você prefere usar para escrever o seu código do lado do cliente?


Peter Vogel é um diretor no PH&V Information Services, especializado em desenvolvimento Web com experiência em SOA, desenvolvimento do lado do cliente e design de interface do usuário. Os clientes PH&V incluem o Canadian Imperial Bank of Commerce, Volvo e Microsoft. Ele também ensina e escreve cursos para Learning Tree Internacional e escreve a coluna .NET Prática para VisualStudioMagazine.com.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Ryan Cavanaugh