Compartilhar via


TypeScript

Aprimore seu investimento no JavaScript com TypeScript

Bill Wagner

Baixar o código de exemplo

A linguagem de programação do TypeScript é, na verdade, um subconjunto adequado do JavaScript. Se você estiver usando o JavaScript, você já está escrevendo o TypeScript. Isto não significa que você está escrevendo um bom TypeScript e usando todos os seus recursos. Significa que você tem um caminho de migração tranquilo de seu investimento no JavaScript para uma base de códigos do TypeScript que aproveita os novos recursos que o TypeScript oferece.

Neste artigo, vou fornecer recomendações sobre migrar um aplicativo do JavaScript para o TypeScript. Você aprenderá como migrar do JavaScript para o TypeScript para usar o sistema de tipo Typescript para ajudá-lo a escrever códigos melhores. Com a análise estática do TypeScript, você minimizará os erros e será mais produtivo. Ao seguir estas recomendações, você também reduzirá a quantidade de erros e avisos do sistema de tipo TypeScript durante a migração.

Começarei com um aplicativo que gerencia um catálogo de endereços como um exemplo. É um SPA (Aplicativo de Página Única) usando o JavaScript no cliente. Mantive simples para este artigo e somente inclui a parte que exibe uma lista de contatos. Ele usa a estrutura Angular para a vinculação de dados e suporte a aplicativo. A estrutura Angular manipula a vinculação de dados e modelagem de texto para exibir as informações de contato.

Três arquivos do JavaScript compõem o aplicativo: O arquivo app.js contém o código que inicia o aplicativo. O arquivo contactsController.js é o controlador para a página de listagem. O arquivo contactsData.js contém uma lista de contatos que será exibida. O controlador—juntamente com a estrutura Angular—manipula o comportamento da página de listagem. Você pode classificar os contatos e mostrar ou ocultar detalhes para um único contato. O arquivo contactsData.js é um conjunto de contatos codificado. Em um aplicativo de produção, este arquivo conteria um código para chamar um servidor e recuperar dados. A lista de contatos codificada torna uma demonstração mais autônoma.

Não se preocupe se você não tiver muita experiência com o Angular. Você verá como é fácil usá-lo quando eu inicio a migração do aplicativo. O aplicativo segue as convenções do Angular, que são fáceis de preservar a medida em que você migra um aplicativo para o TypeScript.

O melhor local para iniciar a migração de um aplicativo para o TypeScript é com o arquivo controlador. Porque qualquer código do JavaScript válido é também um código do TypeScript válido, basta alterar a extensão do arquivo controlador chamado contactsController.js de .js para .ts. A linguagem do TypeScript é um cidadão de primeira classe na Atualização 2 para o Visual Studio 2013. Se você tem a extensão Web Essentials instalada, você verá tanto a fonte do TypeScript como o resultado do JavaScript gerado na mesma janela (veja a Figura 1).

The TypeScript Editing Experience in Visual Studio 2013 Update 2
Figura 1 A experiência de edição do TypeScript na Atualização 2 para Visual Studio 2013

Porque os recursos de linguagem específicos do TypeScript ainda não estão sendo usados, estes dois modos de exibição são quase os mesmos. A linha de comando adicional no final fornece informações para o Visual Studio ao depurar aplicativos do TypeScript. Ao usar o Visual Studio, um aplicativo pode ser depurado no nível do TypeScript, ao invés de gerar o nível de fonte do JavaScript.

Você pode ver o compilador do TypeScript informar um erro para este aplicativo, embora o compilador gere um resultado do JavaScript válido. Este é um dos grandes recursos da linguagem do TypeScript. É uma consequência natural da regra que o TypeScript é um superconjunto estrito do JavaScript. Eu ainda não declarei o símbolo contactsApp em nenhum arquivo do TypeScript. No entanto, o compilador do TypeScript assume o tipo any, e assume que o símbolo referenciará um objeto no tempo de execução. Apesar destes erros, eu posso executar o aplicativo e ele ainda funcionará corretamente.

Eu poderia continuar e alterar as extensões de todos os arquivos JavaScript no aplicativo. Mas eu não recomendaria fazer isso ainda, porque haverá muito mais erros. O aplicativo ainda irá funcionar, mas tendo tanto erros torna mais difícil usar o sistema TypeScript para ajudá-lo a escrever códigos melhores. Eu prefiro trabalhar em um arquivo por vez e adicionar informações do tipo ao aplicativo enquanto avanço. Desta maneira tenho um número menor de erros no sistema de tipo para corrigir de uma vez. Depois de ter uma compilação limpa, eu sei que o compilador do TypeScript está me ajudando a evitar estes erros.

É fácil de declarar uma variável externa para o contactsApp. Por padrão, ela teria o tipo any:

declare var contactsApp: any;

Enquanto isto corrige o erro do compilador, não ajuda a evitar erros ao chamar métodos na biblioteca do Angular. O tipo any é simplesmente o que parece: Pode ser qualquer coisa. O TypeScript não realizará nenhum tipo de verificação quando você acessa a variável contactsApp. Para obter a verificação de tipo, você precisa contar ao TypeScript sobre o tipo de contactsApp e sobre os tipos definidos na estrutura Angular.

O TypeScript habilita as informações de tipo para bibliotecas do JavaScript existentes com um recurso chamado de Definições de Tipo. Uma Definição de Tipo é um conjunto de declarações sem implementação. Elas descrevem os tipos e suas APIs para o compilador do TypeScript. O projeto DefinitelyTyped no GitHub tem definições de tipo para várias bibliotecas do JavaScript populares, incluindo Angular.js. Eu inclui estas definições no projeto usando o gerenciador de pacotes NuGet.

Assim que as Definições de Tipo e a biblioteca Angular tiverem sido incluídas, posso usá-las para corrigir os erros do compilador que estou vendo. Eu preciso referenciar as informações de tipo que acebei de adicionar ao projeto. Há um comentário especial que diz para o compilador do TypeScript para fazer referência as informações do tipo:

/// <reference path="../Scripts/typings/angularjs/angular.d.ts" />

O compilador do TypeScript agora pode interpretar qualquer um dos tipos definidos no arquivo Definição de Tipo angular.d.ts. Está na hora de corrigir o tipo da variável contactsApp. O tipo esperado da variável contactsApp, que é declarada no app.js no namespace ng, é um IModule:

declare var contactsApp: ng.IModule;

Com esta declaração, eu obterei o IntelliSense sempre que um período for pressionado após contactsApp. Eu também vou obter relatórios de erro do compilador do TypeScript sempre que digitar errado ou usar de forma errada as APIs declaradas no objeto contactsApp. Os erros do compilador desapareceram e eu inclui informação estática sobre o tipo para o objeto do aplicativo.

O restante do código no objeto contactsController ainda está faltando as informações de tipo. Até você adicionar anotações de tipo, o compilador do TypeScript irá supor que qualquer variável é do tipo any. O segundo parâmetro para o método contactsApp.controller é uma função e o primeiro parâmetro daquela função, o $scope, é do tipo ng.IScope. Então, incluirei aquele tipo na declaração da função (contactData continuará a ser interpretado como tipo any):

contactsApp.controller('ContactsController', function ContactsController($scope : ng.IScope, contactData) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; } });

Isto introduz um novo conjunto de erros do compilador. Os novo erros são porque o código dentro da função contactsController manipula as propriedades que não fazem parte do tipo ng.IScope. O Ng.IScope é uma interface e o objeto $scope real é um tipo específico do aplicativo que implementa o IScope. Estas propriedades sendo manipuladas são membros daquele tipo. Para aproveitar o tipo estático do TypeScript, eu preciso definir aquele tipo específico do aplicativo. Vou chamá-lo de IContactsScope:

interface IContactsScope extends ng.IScope { sortOrder: string; hideMessage: string; showMessage: string; contacts: any; toggleShowDetails: (contact: any) => boolean; }

Assim que a interface for definida, eu simplesmente altero o tipo da variável $scope na declaração de função:

function ContactsController($scope : IContactsScope, contactData) {

Após fazer estas alterações, eu posso criar o aplicativo sem erros e ele executará corretamente. Há vários conceitos importantes para observar ao adicionar esta interface. Observe que não tive que encontrar nenhum outro código e declarar que nenhum tipo específico implementa o tipo IContactsScope. O TypeScript oferece suporte ao tipo estrutural, coloquialmente conhecida como “tipo pato.” Isto significa que qualquer objeto que declara as propriedades e métodos declarados no IContactsScope implementa a interface do IContactsScope, quer aquele tipo declare ou não que implementa o IContactsScope.

Observe que estou usando o tipo any do TypeScript como um espaço reservado na definição do IContactsScope. A propriedade de contatos representa a lista de contatos e eu ainda não fiz a migração do tipo Contact. Eu posso usar qualquer como o espaço reservado e o compilador do TypeScript não realizará nenhum tipo de verificação no acesso destes valores. Está é uma técnica útil durante toda a migração do aplicativo.

O tipo any representa quaisquer tipos que ainda não tiver migrado do JavaScript para o TypeScript. Ele torna a migração mais tranquila com menos erros do compilador do TypeScript para corrigir cada iteração. Eu também posso pesquisar por variáveis declaradas como tipo any e encontrar trabalho que ainda tenho que realizar. O “Any” diz para o compilador do TypeScript para não realizar uma verificação do tipo any naquela variável. Pode ser qualquer coisa. O compilador assumirá que você conhece as APIs disponíveis naquela variável. Isso não quer dizer que cada uso de “any” é ruim. Há usos válidos para o tipo any, como quando uma API do JavaScript é projetada para trabalhar com diferentes tipos de objetos. Ao usar o “any” como um espaço reservado durante uma migração é apenas uma das boas formas.

Finalmente, a declaração da toggleShowDetails mostra como as declarações de função são representadas no TypeScript:

toggleShowDetails: (contact: any) => boolean;

O nome da função é toggleShowDetails. Depois da vírgula, você verá a lista do parâmetro. Esta função usa um único parâmetro, atualmente do tipo any. O nome “contact” é opcional. Você pode usar este para fornecer mais informações para outros programadores. A seta gorda aponta para o tipo de retorno, que é um booleano neste exemplo.

Tendo introduzido o tipo any na definição IContactScope mostra a você onde ir para trabalhar depois. O TypeScript ajuda você a evitar erros quando você o fornece mais informações sobre os tipos com os quais você está trabalhando. Eu substituirei aquele any por qualquer definição melhor do que a que está em um Contact ao definir um tipo IContact que inclui as propriedades disponíveis em um objeto de contato (veja a Figura 2).

Figura 2 Incluindo propriedades em um objeto de contato

interface IContact { first: string; last: string; address: string; city: string; state: string; zipCode: number; cellPhone: number; homePhone: number; workPhone: number; showDetails: boolean }

Com a interface IContact agora definida, eu a usarei para a interface IContactScope:

interface IContactsScope extends ng.IScope { sortOrder: string; hideMessage: string; showMessage: string; contacts: IContact[]; toggleShowDetails: (contact: IContact) => boolean; }

Eu não preciso adicionar as informações de tipo na definição da função toggleShowDetails definidas na função contactsController. Porque a variável $scope é um IContactsScope, o compilador do TypeScript sabe que a função atribuída ao toggleShowDetails deve corresponder ao protótipo da função no IContactScope e o parâmetro deve ser um IContact.

Observe o JavaScript gerado para esta versão do contactsController na Figura 3. Observe que todos os tipos de interface que defini foram removidad do JavaScript gerado. As anotações de tipo existem para você e para as ferramentas de análise estáticas. Estas anotações não executam para o JavaScript gerado porque não são necessárias.

Figura 3 A versão do TypeScript do controlador e do JavaScript gerado

/// reference path="../Scripts/typings/angularjs/angular.d.ts" var contactsApp: ng.IModule; interface IContact { first: string; last: string; address: string; city: string; state: string; zipCode: number; cellPhone: number; homePhone: number; workPhone: number; showDetails: boolean } interface IContactsScope extends ng.IScope { sortOrder: string; hideMessage: string; showMessage: string; contacts: IContact[]; toggleShowDetails: (contact: IContact) => boolean; } contactsApp.controller('ContactsController', function ContactsController($scope : IContactsScope, contactData) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; return contact.showDetails; } }); // Generated JavaScript /// reference path="../Scripts/typings/angularjs/angular.d.ts" var contactsApp; contactsApp.controller('ContactsController', function ContactsController($scope, contactData) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; return contact.showDetails; }; }); //# sourceMappingURL=contactsController.js.map

Adicionar definições de classe e módulo

Adicionar anotações de tipo ao seu código habilita as ferramentas de análise estática encontrar e relatar possíveis erros que você fez no seu código. Isto abrange tudo do IntelliSense a análise lint-like, para aviso e erros de tempo de compilação.

Outra importante vantagem que o TypeScript fornece a mais que o JavaScript é uma sintaxe melhor para tipos de escopo. A palavra-chave module do TypeScript permite que você coloque definições de tipo dentro de um escopo e evite colisões com tipos de outros módulos que podem usar o mesmo nome.

O aplicativo de exemplo de contatos não é grande, mas ainda é uma boa ideia colocar as definições de tipo em módulos para evitar colisões. Aqui, eu colocarei o contactsController e outros tipos que defini dentro de um módulo chamado de Rolodex:

module Rolodex { // Elided }

Eu não adicionei a palavra-chave de exportação em nenhuma definição neste módulo. Isso significa que os tipos definidos dentro do módulo Rolodex podem somente ser referenciados de dentro daquele módulo. Eu adicionarei a palavra-chave de exportação nas interfaces definidas neste módulo usarei estes tipos mais tarde a medida em que eu migrar os códigos do Data. Eu também alterarei o código para o ContactsController de uma função para uma classe. Esta classe precisa de um construtor para inicializar sozinha, mas nenhum outro método público (veja a Figura 4).

Figura 4 Alterar o ContactsController de uma função para uma classe

export class ContactsController { constructor($scope: IContactsScope, contactData: any) { $scope.sortOrder = 'last'; $scope.hideMessage = "Hide Details"; $scope.showMessage = "Show Details"; $scope.contacts = contactData.getContacts(); $scope.toggleShowDetails = function (contact) { contact.showDetails = !contact.showDetails; return contact.showDetails; } } }

Criar este tipo agora altera a chamada para contactsApp.controller. O segundo parâmetro é agora o tipo de classe, não a função definida anteriormente. O primeiro parâmetro da função do controlador é o nome do controlador. O Angular mapeia os nomes do controlador para as funções do controlador. Em qualquer lugar na página HTML onde o tipo ContactsController é referenciado, o Angular chamará o construtor para a classe ContactsController:

contactsApp.controller('ContactsController', Rolodex.ContactsController);

O tipo de controlador foi agora migrado completamente do JavaScript para o TypeScript. A nova versão contém anotações de tipo para tudo definido ou usado no controlador. No TypeScript, eu poderia fazer isso sem a necessidade de alterações em outras partes do aplicativo. Nenhum outro arquivo foi afetado. Combinar o TypeScript com o JavaScript é tranquilo, o que simplifica adicionar o TypeScript a um aplicativo do JavaScript existente. O sistema de tipo TypeScript depende do tipo de interface e do tipo estrutural, que facilita uma interação fácil entre o TypeScript e o JavaScript.

Agora, avançarei para o arquivo contactData.js (veja a Figura 5). Esta função usa o método do alocador do Angular para retornar um objeto que retorna uma lista de contatos. Como o controlador, o método do alocador mapeia nomes (contactData) para uma função que retorna o serviço. Esta convenção é usada no construtor do controlador. O segundo parâmetro do construtor é chamado de contactData. O Angular usa este nome de parâmetro para mapear para o alocador adequado. Como você pode ver, a estrutura Angular é baseada em convenção.

Figura 5 A versão do JavaScript do serviço contactData

'use strict'; contactsApp.factory('contactData', function () { var contacts = [ { first: "Tom", last: "Riddle", address: "66 Shack St", city: "Little Hangleton", state: "Mississippi", zipCode: 54565, cellPhone: 6543654321, homePhone: 4532332133, workPhone: 6663420666 }, { first: "Antonin", last: "Dolohov", address: "28 Kaban Ln", city: "Gideon", state: "Arkensas", zipCode: 98767, cellPhone: 4443332222, homePhone: 5556667777, workPhone: 9897876765 }, { first: "Evan", last: "Rosier", address: "28 Dominion Ave", city: "Notting", state: "New Jersey", zipCode: 23432, cellPhone: 1232343456, homePhone: 4432215565, workPhone: 3454321234 } ]; return { getContacts: function () { return contacts; }, addContact: function(contact){ contacts.push(contact); return contacts; } }; })

Novamente, a primeira etapa é simplesmente alterar a extensão de .js para .ts. Ele compila de forma limpa e o JavaScript gerado corresponde estreitamente ao arquivo do TypeScript de origem. Depois, colocarei o código no arquivo contactData.ts no mesmo módulo Rolodex. Isto abrange todos os códigos para o aplicativo na mesma partição lógica.

Depois, migrarei o alocador do contactData para uma classe. Declare a classe com o tipo ContactDataServer. Ao invés de uma função que retorna um objeto com duas propriedades que são os métodos, eu posso simplesmente definir os métodos como membros de um objeto do ContactDataServer. Os dados iniciais são agora um membro de dados de um objeto do tipo ContactDataServer. Eu também preciso usar este tipo na chamada para o contactsApp.factory:

contactsApp.factory('contactsData', () => new Rolodex.ContactDataServer());

O segundo parâmetro é uma função que retorna um novo Contact­DataServer. O alocador criará o objeto quando eu precisar. Se eu tentar compilar e executar esta versão, eu terei erros do compilador, pois o tipo ContactDataServer não é exportado do módulo Rolodex. É, no entanto, referenciado na chamada para o contacts­App.factory. Este é outro exemplo de como o sistema de tipo TypeScript é muito complacente, o que torna a tarefa de migração muito mais fácil. Eu posso facilmente corrigir este erro adicionando a palavra-chave de exportação à declaração de classe ContactDataServer.

É possível ver a versão final na Figura 6. Observe que adicionei a informação de tipo para matriz de contatos e para o parâmetro de entrada no método addContact. As anotações de tipo são opcionais—o TypeScript é válido sem elas. No entanto, eu encorajaria você a adicionar todas as informações de tipo necessárias ao seu código do TypeScript, pois ajuda a evitar erros no sistema TypeScript. que permitirá que você seja mais produtivo.

Figura 6 A versão do TypeScript do ContactDataServer

/// reference path="../Scripts/typings/angularjs/angular.d.ts" var contactsApp: ng.IModule; module Rolodex { export class ContactDataServer { contacts: IContact[] = [ { first: "Tom", last: "Riddle", address: "66 Shack St", city: "Little Hangleton", state: "Mississippi", zipCode: 54565, cellPhone: 6543654321, homePhone: 4532332133, workPhone: 6663420666, showDetails: true }, { first: "Antonin", last: "Dolohov", address: "28 Kaban Ln", city: "Gideon", state: "Arkensas", zipCode: 98767, cellPhone: 4443332222, homePhone: 5556667777, workPhone: 9897876765, showDetails: true }, { first: "Evan", last: "Rosier", address: "28 Dominion Ave", city: "Notting", state: "New Jersey", zipCode: 23432, cellPhone: 1232343456, homePhone: 4432215565, workPhone: 3454321234, showDetails: true } ]; getContacts() { return this.contacts; } addContact(contact: IContact) { this.contacts.push(contact); return this.contacts; } } } contactsApp.factory('contactsData', () => new Rolodex.ContactDataServer());

Agora que criei uma nova classe ContactDataServer, farei uma última alteração ao controlador. Lembre-se de que o segundo parâmetro do construtor contactsController foi o servidor de dados. Agora, posso tornar isso mais seguro declarando que o seguinte parâmetro deve ser do tipo ContactDataServer:

constructor($scope: IContactsScope, contactData: ContactDataServer) {

Migrações tranquilas do JavaScript para o TypeScript

O TypeScript tem muito mais recursos do que estes que demonstrei aqui. Conforme você trabalha com o TypeScript, você adotará seus recursos. Quanto mais você usa as extensões do TypeScript para o JavaScript, mais a sua produtividade aumentará. Lembre-se de que as anotações de tipo do TypeScript foram criadas para fornecer uma migração tranquila do JavaScript para o TypeScript. Mais importante, tenha em mente que o TypeScript é um superconjunto estrito do JavaScript. Isto significa que qualquer JavaScript válido é um TypeScript válido.

Além disso, as anotações de tipo do TypeScript têm pouca cerimônia. As anotações de tipo são verificadas onde você as fornece e você não é forçado a adicioná-las em todos os lugares. Conforme você migra do JavaScript para o TypeScript, isto é muito útil. 

Finalmente, o sistema de tipo TypeScript dá suporte para a composição de tipo estrutural. Conforme você define interfaces para tipos importantes, o sistema de tipo TypeScript assumirá qualquer objeto com estes métodos e propriedades que fornecem suporte àquela interface. Não é necessário declarar o suporte da interface em cada definição de classe. Objetos anônimos podem também dar suporte as interfaces usando este recurso de composição de tipo estrutural.

Estes recursos combinados criam uma caminho tranquilo conforme você migra sua base de códigos do JavaScript para o TypeScript. Quando mais longe você chega no caminho da migração, mais benefícios você obterá da análise de código estática do TypeScript. O seu objetivo final deveria ser aproveitar o máximo possível da segurança do TypeScript. Ao longo do caminho, suas funções de código do Java Script existentes como TypeScript válido não faz sentido usar as anotações de tipo do TypeScript. É quase um processo sem atrito. Você não tem motivo para não usar o TypeScript em seus aplicativos atuais do JavaScript.

Bill Wagner é autor do best-seller, “Effective C#” (2004), agora em sua segunda edição, e “More Effective C#” (2008), ambos da Addison-Wesley Professional. Ele também gravou vídeos para a Pearson Education informIT, “C# Async Fundamentals LiveLessons” e “C# Puzzlers.” Ele publica ativamente no blog thebillwagner.com e pode ser encontrado em bill.w.wagner@outlook.com.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Jonathan Turner
Jonathan Turner é o gerente de programas da equipe do TypeScript na Microsoft e um co-designer da linguagem TypeScript.  Antes de se unir à Microsoft, ele trabalhou na linguagem de programação Clang/LLVM e Chapel.