TypeScript para desenvolvimento de back-end

A linguagem Java ainda reina suprema no desenvolvimento de back-end. Há muitos motivos para isso: velocidade, segurança (se, é claro, fechar os olhos para ponteiros nulos), além de um ecossistema extenso e bem testado. Mas na era dos microsserviços e do desenvolvimento ágil, outros fatores se tornaram mais importantes. Em alguns sistemas, pode não ser necessário manter o desempenho máximo e ter um ecossistema robusto de dependências estáveis ​​quando se trata de um serviço simples que executa operações CRUD e transformação de dados. Além disso, muitos sistemas devem ser construídos e reconstruídos rapidamente para acompanhar o rápido desenvolvimento iterativo de recursos.



É fácil desenvolver e implantar um serviço Java simples, graças à magia penetrante do Spring Boot. Mas, como as classes fechadas precisam ser testadas e os dados transformados, os construtores, conversores, construtores de enumeração e serializadores são abundantes em seu código e abrem caminho para o inferno do código Java estereotipado. É por isso que o desenvolvimento de novos recursos costuma ser atrasado. E, sim, a geração de código funciona, mas não é muito flexível.



O TypeScript ainda não se estabeleceu bem entre os desenvolvedores de back-end. Provavelmente porque é conhecido como um conjunto de arquivos declarativos que permitem adicionar alguma digitação ao JavaScript. Mesmo assim, há uma tonelada de lógica que exigiria dezenas de linhas de Java para representar e que pode ser representada em apenas algumas linhas do TypeScript.

Muitos dos recursos chamados de recursos característicos do TypeScript, na verdade, se referem ao JavaScript. Mas o TypeScript também pode ser visto como uma linguagem própria, com algumas semelhanças sintáticas e conceituais com o JavaScript. Então, vamos desviar do JavaScript por um momento e dar uma olhada no TypeScript por si só: é uma bela linguagem com um sistema de tipos extremamente poderoso, mas flexível, toneladas de açúcar sintático e, finalmente, segurança nula! Hospedamos



um repositório no Github com um aplicativo da web Node / TypeScript customizado, junto com algumas explicações adicionais. Há também um ramo avançado com um exemplo de arquitetura onion e mais conceitos de digitação não triviais.



Apresentando TypeScript



Vamos começar com o básico: TypeScript é uma linguagem de programação funcional assíncrona que, no entanto, oferece suporte a classes e interfaces, bem como a atributos públicos, privados e protegidos. Portanto, ao trabalhar com essa linguagem, um programador ganha considerável flexibilidade para trabalhar no nível de microarquitetura e estilo de código. O compilador TypeScript pode ser configurado dinamicamente, ou seja, controlar quais tipos de importações são permitidas, se as funções exigem tipos de retorno explícitos e se as verificações de zero são habilitadas em tempo de compilação.



Como o TypeScript é compilado para JavaScript regular, o Node.js é usado como o tempo de execução de backend. Na ausência de uma estrutura abrangente que se pareça com o Spring, um serviço da web típico usaria uma estrutura mais flexível servindo como um servidor da web ( Express.js é um ótimo exemplo disso ). Conseqüentemente, ele se tornará menos "mágico" e sua configuração e configuração básicas serão organizadas de forma mais explícita. Nesse caso, serviços relativamente complexos também exigirão mais ajustes na configuração. Por outro lado, configurar aplicativos relativamente pequenos não é difícil e, além disso, é viável quase sem aprender o framework.



O gerenciamento de dependências é fácil com o gerenciador de pacotes flexível, porém poderoso, do Node, o npm.



O básico



Ao definir classes public, modificadores de controle de acesso são suportados protectede private, bem conhecidos pela maioria dos desenvolvedores:



class Order {

    private status: OrderStatus;

    constructor(public readonly id: string, isSpecialOrder: boolean) {
        [...]
    }
}


A classe agora tem Orderdois atributos: um campo privado statuse um campo público idsomente leitura. À máquina, argumentos do construtor com palavras-chave public, protectedou privateatributos de classe tornam-se automaticamente.



interface User {
    id?: string;
    name: string;
    t_registered: Date;
}

const user: User = { name: 'Bob', t_registered: new Date() };


Observe que, como o TypeScript usa inferência de tipo, o objeto User pode ser instanciado mesmo se a Userprópria classe não for fornecida . Essa abordagem semelhante a uma estrutura geralmente é escolhida ao trabalhar com entidades de dados puros e não requer nenhum método ou estado interno.



Os genéricos são expressos em TypeScript da mesma maneira que em Java:



class Repository<T extends StoredEntity> {
    findOneById(id: string): T {
        [...]
    }
}


Sistema de tipo poderoso



No coração do poderoso sistema de tipos do TypeScript está a inferência de tipos; ele também suporta tipagem estática. No entanto, as anotações de tipo estático são opcionais se o tipo de retorno ou tipo de parâmetro puder ser inferido do contexto.



O TypeScript também permite o uso de tipos de união, tipos parciais e intersecções de tipo, o que dá flexibilidade considerável à linguagem, evitando complexidade desnecessária. No TypeScript, você também pode usar um valor específico como um tipo, o que é extremamente útil em uma variedade de situações.



Enumerações, inferência de tipo e tipos de união



Considere uma situação comum em que o status do pedido precisa ter uma representação de tipo seguro (como uma enumeração), mas uma representação de string também é necessária para serialização JSON. Em Java, isso seria um enum, junto com um construtor e um getter para valores de string.



No primeiro exemplo, enums do TypeScript permitem adicionar diretamente uma representação de string. Isso nos deixa com uma representação de enumeração de tipo seguro que serializa automaticamente sua representação de string associada.



enum Status {
    ORDER_RECEIVED = 'order_received',
    PAYMENT_RECEIVED = 'payment_received',
    DELIVERED = 'delivered',
}

interface Order {
    status: Status;
}

const order: Order = { status: Status.ORDER_RECEIVED };


Observe a última linha do código, onde a inferência de tipo nos permite instanciar um objeto que corresponda à interface `Order`. Como não há necessidade de colocar nenhum estado interno ou lógica em nosso pedido, podemos fazer sem classes e sem construtores.



É verdade que, ao compartilhar a inferência de tipos e tipos de união entre si, essa tarefa pode ser resolvida ainda mais facilmente:



interface Order {
    status: 'order_received' | 'payment_received' | 'delivered';
}

const orderA: Order = { status: 'order_received' }; // 
const orderB: Order = { status: 'new' }; //  


O compilador TypeScript só aceitará a string que foi fornecida a ele como um status de pedido válido (observe que isso ainda exigirá a validação do JSON de entrada).



Basicamente, essas representações de tipo funcionam com qualquer coisa. Um tipo pode muito bem ser uma união de um literal de string, um número e qualquer outro tipo ou interface personalizada. Para obter exemplos mais interessantes, consulte o Advanced Typing Guide do TypeScript .



Lambdas e argumentos funcionais



Como o TypeScript é uma linguagem de programação funcional, ele tem suporte para funções anônimas, também chamadas de lambdas, em seu núcleo.



const evenNumbers = [ 1, 2, 3, 4, 5, 6 ].filter(i => i % 2 == 0);


O exemplo acima .filter()usa uma função do tipo (a: T) => boolean. Esta função é representada por um lambda anônimo i => i % 2 == 0. Ao contrário do Java, onde os parâmetros funcionais devem ter um tipo explícito, interface funcional, o tipo lambda também pode ser representado anonimamente:



class OrderService {
    constructor(callback: (order: Order) => void) {
        [...]
    }
}


Programação assíncrona



Como o TypeScript, com todas as ressalvas, é um superconjunto do JavaScript, a programação assíncrona é um conceito-chave nesta linguagem. Sim, você pode usar lambdas e callbacks aqui, o TypeScript tem dois mecanismos principais para ajudá-lo a evitar o inferno de callback: promessas e um padrão bonito async/await. Uma promessa é essencialmente um valor de retorno imediato que promete retornar um valor específico posteriormente.



//  ,  
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
    [...]
}

//     
function getActiveProfiles(): Promise<UserProfile[]> {
    return fetchUserProfiles(URL)
        .then(profiles => profiles.filter(profile => profile.active))
        .catch(error => handleError(error));
}


Como as instruções .then()podem ser encadeadas em qualquer número, em alguns casos o padrão acima pode levar a um código bastante confuso. Ao declarar uma função asynce usá-la awaitenquanto espera a promessa ser resolvida, você pode escrever esse mesmo código em um estilo muito mais síncrono. Também neste caso, abre-se uma oportunidade para usar operadores bem conhecidos try/catch:



//  async/await ( ,  fetchUserProfiles  )
async function getActiveProfiles(): Promise<UserProfile[]> {
    const allProfiles = await fetchUserProfiles(URL);
    return allProfiles.filter(profile => profile.active);
}

//   try/catch
async function getActiveProfilesSafe(): Promise<UserProfile[]> {
    try {
        const allProfiles = await fetchUserProfiles(URL);
        return allProfiles.filter(profile => profile.active);
    } catch (error) {
        handleError(error);
        return [];
    }
}


Observe que, embora o código acima pareça ser síncrono, ele é apenas visível (já que outra promessa é retornada aqui).



Operador de extensão e operador de repouso: tornando sua vida mais fácil



Ao usar Java, a manipulação de dados, construção, fusão e desestruturação de objetos geralmente produzem código estereotipado em grandes quantidades. As classes devem ser definidas, os construtores, getters e setters devem ser gerados e os objetos devem ser instanciados. Em casos de teste, muitas vezes é necessário recorrer ativamente à reflexão sobre instâncias simuladas de classes fechadas.



No TypeScript, tudo isso pode ser tratado sem esforço com seu doce açúcar sintático de tipo seguro: operadores de propagação e repouso.



Primeiro, vamos usar o operador de expansão de array ... para descompactar o array:



const a = [ 'a', 'b', 'c' ];
const b = [ 'd', 'e' ];

const result = [ ...a, ...b, 'f' ];
console.log(result);

// >> [ 'a', 'b', 'c', 'd', 'e', f' ]


Isso é conveniente, é claro, mas o TypeScript real começa quando você percebe que pode fazer o mesmo com objetos:



interface UserProfile {
    userId: string;
    name: string;
    email: string;
    lastUpdated?: Date;
}

interface UserProfileUpdate {
    name?: string;
    email?: string;
}

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const update: UserProfileUpdate = { email: 'bob@example.com' };

const updated: UserProfile = { ...userProfile, ...update, lastUpdated: new Date() };

console.log(updated);

// >> { userId: 'abc', name: 'Bob', email: 'bob@example.com', lastUpdated: 2019-12-19T16:09:45.174Z}


Vamos ver o que está acontecendo aqui. Basicamente, um objeto updatedé criado usando o construtor de chaves. Dentro desse construtor, cada parâmetro realmente cria um novo objeto, começando da esquerda.



Portanto, o objeto estendido é usado userProfile; a primeira coisa que ele faz é se copiar. Na segunda etapa, o objeto estendido é updateincorporado a ele e reatribuído ao primeiro objeto; isso, novamente, cria um novo objeto. Na última etapa, o campo é mesclado e reatribuído lastUpdated, em seguida, um novo objeto é criado e, como resultado, o objeto final.



Usar o operador de propagação para criar cópias de um objeto imutável é uma maneira muito segura e rápida de processar dados. Observação: o operador de propagação cria uma cópia superficial do objeto. Elementos com profundidade de mais de um são então copiados como links.



O operador de extensão também tem um destruidor equivalente denominado objeto resto :



const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const { userId, ...details } = userProfile;
console.log(userId);
console.log(details);

// >> 'abc'
// >> { name: 'Bob', email: 'bob@example.com' }


Agora é a hora de apenas sentar e imaginar todo o código que você teria que escrever em Java para realizar as operações mostradas acima.



Conclusão. Um pouco sobre as vantagens e desvantagens



atuação



Como o TypeScript é inerentemente assíncrono e tem um ambiente de tempo de execução rápido, há muitos cenários em que um serviço Node / TypeScript pode competir com um serviço Java. Essa pilha é especialmente boa para operações de E / S e funcionará bem com operações de bloqueio curtas ocasionais, como redimensionar uma nova imagem de perfil. No entanto, se o objetivo principal de um serviço é fazer alguns cálculos sérios na CPU, Node e TypeScript provavelmente não são muito adequados para isso.



Tipo de número



O tipo usado no TypeScript também deixa muito a desejar number, o que não faz distinção entre valores inteiros e de ponto flutuante. A prática mostra que em muitas aplicações isso não apresenta absolutamente nenhum problema. No entanto, é melhor não usar o TypeScript se você estiver escrevendo um aplicativo para uma conta bancária ou serviço de checkout.



Ecossistema



Dada a popularidade do Node.js, não deve ser surpresa que existam centenas de milhares de pacotes para ele hoje. Mas, como o Node é mais jovem que o Java, muitos pacotes não sobreviveram a tantas versões e a qualidade do código em algumas bibliotecas é claramente ruim.



Entre outras, vale a pena mencionar algumas bibliotecas de alta qualidade que são muito convenientes para trabalhar: por exemplo, para servidores web , injeção de dependência e anotações de controlador . Mas, se o serviço depender seriamente de vários programas de terceiros bem suportados, então é melhor usar Python, Java ou Clojure.



Desenvolvimento acelerado de recursos



Como vimos acima, uma das vantagens mais importantes do TypeScript é a facilidade de expressar lógicas, conceitos e operações complexas nessa linguagem. O fato de JSON ser parte integrante desta linguagem, e hoje ser amplamente utilizado como formato de serialização de dados para transferência de dados e trabalho com bancos de dados orientados a documentos, em tais situações parece natural recorrer ao TypeScript. Configurar um servidor Node é muito rápido, geralmente sem dependências desnecessárias; isso irá economizar recursos do sistema. É por isso que a combinação de Node.js com o sistema de tipos fortes do TypeScript é tão eficaz para criar novos recursos rapidamente.



Finalmente, o TypeScript é bem temperado com açúcar sintático, então o desenvolvimento com ele é bom e rápido.



All Articles