Funções

Funções

Jornada Javascript: Funções

O conteúdo dessa lição se baseia nas funções que aprendemos na 6° lição da jornada Javascript. Link abaixo!

Para acessar o link da lição, clique aqui.  

Olá leitor, seja bem vindo a mais uma lição da jornada do desenvolvedor Typescript 😎

Hoje nós iremos nos aprofundar um pouco mais sobre o uso de funções usando o superset Typescript, vamos nessa?

Trabalhando com Funções em Typescript

Em lições passadas, você chegou a ver o uso de algumas funções e seus possíveis retornos usando puramente Typescript.

Nesta lição, nos iremos recapitular e nos aprofundar sobre os conceitos que vimos anteriormente 😉

Tudo isso para que você possa dominar de uma vez por todas, o uso de funções na linguagem Typescript!

Declarando Funções sem Retorno

No Javascript, a coisa mais comum de se criar são funções que não apresentam nenhum tipo de retorno, como por exemplo:

function olaMundo(){
 console.log('Olá Mundo!');
}

olaMundo();

No exemplo acima, declaramos uma função que mostra uma simples mensagem no console.

Já no Typescript, quando declaramos uma função sem retorno, o ideal é usar o comando voidlogo após o fechamento dos parêntesis:

function olaMundo(): void{
 console.log('Olá Mundo!');
}

olaMundo();

O void é um termo usado para indicar que uma função ou um método de uma classe, não retorna nenhum valor.

Ele esta presente também em diversas outras linguagens mais antigas como: C/C++, Java e C#, e foi pela influência delas que o void também está presente no TS.

Na visão do desenvolvedor, uma função que faz o uso do termo void, já é considerado um bom adianto, pois nós diz de forma clara que aquela função realmente não vai retornada nada.

Diferente de uma função sem retorno que não especifica nada como acontece no Javascript:

function olaMundo(){
 console.log('Olá Mundo!');
}

function olaMicilini(){
 return 'Roll';
}

Olhando de forma exclusiva para o nome de ambas as função, em um primeiro momento não é possível identificar se aquelas funções terão algum retorno ou não.

Agora veja o mesmo exemplo usando Typescript:

function olaMundo(): void{
 console.log('Olá Mundo!');
}

function olaMicilini(): string{
 return 'Roll';
}

Olhando para ambas as funções, vemos de forma clara que a primeira não retorna nada, e a segunda retorna uma string 🙂

Declarando Funções com Retorno

No Typescript, podemos declarar uma função que retorna algum tipo de dado da seguinte forma, por exemplo:

1) Função que retorna uma string:

function retornaString(): string{
 return 'Olá Mundo!';
}

const mensagem: string = retornaString();//Armazena o valor 'Olá Mundo!'

2) Função que retorna um number:

function somaDoisMaisDois(): number{
 return 2 + 2;
}

const adicao: number = somaDoisMaisDois();//Armazena o valor 4

Note que em todos os casos, estamos informando o tipo de dado que será retornado logo após o fechamento dos parêntesis.

É importante ressaltar que podemos ter diversos tipos de retornos, como arrays, objetos, booleanos, classes e entre outros tipos, bastando apenas seguir a mesma sintaxe dos comandos apresentados acima.

Também é possível fazer com que uma função retorne um Union Type, ou seja, fazer com que ela retorne um ou mais de um tipo de dado, vejamos:

function obterDados(id: number): string | number {
 return "Um"; // Retorna uma string
 return 2;//Se quiser que ela retorne um number só trocar essa linha pela de cima
}

const resultado: string | number = obterDados(2);//Armazena o valor 'Um'

No caso do comando acima, estamos especificando que após o fechamento dos parêntesis, estamos informando que aquela função pode retornar uma string ou um number.

Declarando Funções com Parâmetros

No Typescript, podemos receber parâmetros dentro das nossas funções, assim como vimos em Javascript.

A diferença é que cada um desses parâmetros precisam ser TIPADOS, por exemplo:

1) Função sem retorno que soma dois números:

function somar(a: number, b: number): void{
 const soma = a + b;
 console.log('O resultado da soma é: ' + soma);
}

somar(12, 87);
somar(42, 48);

2) Função com retorno que multiplica dois números:

function multiplicar(a: number, b: number): number{
 const multiplicacao = a * b;
 return multiplicacao;
}

const um: number = multiplicar(12, 87);
const dois: number = multiplicar(42, 48);

3) Função sem retorno que recebe um Union Type:

function processaDados(dado: string | number): void {
 if (typeof dado === "string") {
 console.log("Recebido uma string:", dado);
 } else {
 console.log("Recebido um número:", dado);
 }
}

processaDados("Texto");
processaDados(42);

Lembrando que podemos usar outros tipos de dados como: arrays, booleans, strings, classes e afins.

Declarando Funções com Parâmetros Opcionais

No Typescript, podemos declarar parâmetros opcionais usando o ponto de interrogação (?) logo após o nome da variável temporária, por exemplo:

function exibirDados(nome: string, idade?: number): void {
 console.log(`Nome: ${nome}`);
 if (idade !== undefined) {
 console.log(`Idade: ${idade}`);
 }
}

exibirDados('Micilini Roll', 23);

É importante ressaltar que o primeiro parâmetro nunca pode ser opcional.

Além disso, os parâmetros opcionais devem sempre ser declarados por último.

✅ Declaração correta dos parâmetros opcionais:

function exemplo(param1: string, param2: number, param3?: boolean): void {
 // Implementação
}

❌ Declaração incorreta dos parâmetros opcionais:

function exemplo(param1: string, param3?: boolean, param2: number): void {
 // Implementação
}

No segundo exemplo (incorreto), o parâmetro opcional param3 vem antes do parâmetro obrigatório param2, o que pode levar a confusão, tipo de coisa que não é permitido no TypeScript.

A razão para essa restrição, é que a ordem dos parâmetros ajuda a garantir que os parâmetros obrigatórios sejam fornecidos primeiro, e os parâmetros opcionais possam ser omitidos conforme necessário.

Se a ordem não fosse garantida, seria muito difícil para o compilador e também para nós (desenvolvedores) determinar quais valores seriam passados para quais parâmetros, especialmente em chamadas de função onde alguns parâmetros podem ser omitidos.

Declarando Funções com Parâmetros de Callback

Como você já deve saber (assim espero), no Javascript um callback nada mais é do que uma função passada como argumento para outra função, em que futuramente será chamada de dentro dessa função.

No Typescript, podemos representar isso de forma bem simples. Supondo que temos uma função chamada de olaMundo:

function olaMundo(): void{
 console.log('Olá Mundo!');
}

E que também temos uma outra função, que vai receber a função olaMundo()de modo a executá-la, podemos fazer isso da seguinte forma:

function olaMundo(): void{
 console.log('Olá Mundo!');
}

function mostraMensagem(f: () => void): void{
 console.log('Iniciando a Mensagem...');
 f();//Executa a função recebida por parâmetro
}

mostraMensagem(olaMundo);

No comando acima, criamos uma função chamada de mostraMensagem que recebe como argumento um callback: () => void, que representa a chamada da nossa função: olaMundo(): void.

E como a variável temporária chamada f armazena a função, basta apenas que você a execute: f();.

Agora vamos supor um outro exemplo onde eu queira passar um determinado argumento para a função olaMundo

function olaMundo(nome: string): void{
 console.log('Olá ' + nome);
}

function mostraMensagemDois(f: (nome: string) => void, meuNome: string): void{
 console.log('Iniciando a Mensagem...');
 f(meuNome);//Executa a função recebida por parâmetro
}

mostraMensagemDois(olaMundo, 'Micilini Roll');

No exemplo acima, nós estamos recebendo um parâmetro e chamado nome dentro da função olaMundo(), e obviamente precisamos representar isso  também dentro da função mostraMensagem (f: (nome: string) => void).

Note que estamos recebendo também um segundo parâmetro chamado meuNome dentro da função mostraMensagem, justamente para enviarmos esse parâmetro para dentro do callback para que de lá, ele também possa receber esse valor e mostrar no console 🤓

Agora vejamos um terceiro exemplo onde a função olaMundo retorna uma string:

function olaMundo(nome: string): string{
 return 'Olá ' + nome;
}

function mostraMensagem(f: (nome: string) => string, meuNome: string): void{
 console.log('Iniciando a Mensagem...');
 const mensagem = f(meuNome);//Executa a função recebida por parâmetro
 console.log(mensagem);
}

mostraMensagem(olaMundo, 'Micilini Roll');

Note que o que mudou foi justamente o retorno da função olaMundo():

function olaMundo(nome: string): string{
 .....
}

E também a própria declaração dessa função dentro do mostraMensagem():

f: (nome: string) => string

Fácil, não?

Funções com Parâmetros Padrão (Default)

No Typescript, podemos definir variáveis que já possuem um valor padrão (default) da mesma forma como fazemos no Javascript, observe:

function qualAltura(valor = 80){
 console.log('Altura é: ' + valor);
}

qualAltura();//Seleciona o valor padrão de 80

qualAltura(110);//Define o valor 110

Lembrando que parâmetros com valores default não precisam estar tipados, pois o próprio TS já faz isso por inferência.

Observe um outro exemplo em que temos o segundo valor marcado como default:

function defineTamanho(altura: number, largura = 118){
 console.log('O tamanho é : ' + altura + 'x' + 'largura');
}

defineTamanho(90);//Usa o 118 como largura
defineTamanho(90, 177);//Aqui estamos definindo um valor para largura

Funções Genéricas

No Typescript, temos o Tipo Genérico, na qual nos permite definir uma função que pode receber e retornar quaisquer tipos de dados.

Isso proporciona maior flexibilidade, permitindo que uma função trabalhe com diferentes tipos de dados sem precisar duplicar o código.

A sintaxe básica que usamos para definir um tipo genérico é a seguinte:

function nomeDaFuncao<T>(param: T): T {
 // Implementação
}

No exemplo acima, nós estamos definindo o parâmetro T que representa um tipo genérico. 

Usamos T (ou U) por convênção de código, pois todos os livros e tutoriais em que tipos genéricos são usados, todos preferem usar a letra T ou U.

Mas você também poderiar usar qualquer outra letra ou nome se quisesse, por exemplo:

function nomeDaFuncao<Micilini>(param: Micilini): Micilini {
 // Implementação
}

Agora vamos ver um exemplo de uma função simples que usa o tipo genérico:

function identidade<T>(valor: T): T {
 return valor;
}

// Testando a função com diferentes tipos
const numero = identidade(123); // Tipo inferido: number
const texto = identidade("Hello"); // Tipo inferido: string
const booleano = identidade(true); // Tipo inferido: boolean

console.log(numero); // Saída: 123
console.log(texto); // Saída: Hello
console.log(booleano); // Saída: true

Aqui, a função identidade simplesmente retorna o valor que recebe, e o tipo de T é inferido a partir do valor passado para a função.

Ou seja, é como se naquele momento que recebemos o valor 123 (const numero = identidade(123);) a função identidade transformasse (por de baixo dos panos) o parâmetro T em number.

Basicamente, por de baixo os panos, é assim que acontece:

1° O TypeScript analisa o argumento 123 e determina seu tipo, que é number.

2° Então, ele infere que o tipo T presente na função identidade deva ser do tipo number para essa chamada específica.

É como se disessemos ao TS para "transformar" T em number nesta chamada específica, mas sem realmente modificar o código da função, uma vez que nas próximas chamadas o T pode ser uma string, um boolean ou qualquer outro tipo de dado.

Portanto, o código da função (após a conclusão da chamada) vai permanecer genérico, possibilitando trabalhar com outros tipos diferentes conforme necessário.

Vamos analisar mais um outro exemplo de um tipo genérico que recebe dois parâmetros:

function combinar<T, U>(a: T, b: U): [T, U] {
 return [a, b];
}

// Testando a função com diferentes tipos
const resultado1 = combinar(1, "texto"); // Tipo inferido: [number, string]
const resultado2 = combinar(true, 3.14); // Tipo inferido: [boolean, number]

console.log(resultado1); // Saída: [1, "texto"]
console.log(resultado2); // Saída: [true, 3.14]

Na função combinar, T e U são parâmetros de tipo genérico, além disso, a função retorna um array contendo os dois valores recebidos.

E sim, você pode dizer que um Tipo Genérico se parece muito com o Tipo Any, não é verdade?

Mas será que existem diferenças entre esses dois tipos? É o que iremos descobrir agora  😉

Any X Tipo Genérico

Embora any e tipos genéricos pareçam similares, eles servem a propósitos diferentes e têm características distintas no TypeScript.

O tipo any é um tipo especial em TypeScript onde permite que façamos a atribuição de qualquer tipo a uma determinada variável.

O que consequentemente, desativa a verificação de tipo para aquela variável, o que pode fazer com que alguns erros passem despercebidos durante a execução da nossa aplicação.

Isso faz com que o any deva ser usado como último recurso e com CAUTELA!

Já o Tipo Genérico, permitem que você defina uma função, classe ou interface que trabalha com vários tipos de dados, mas mantém a segurança daquele tipo.

Uma vez que o tipo (por de baixo dos panos) será inferido ainda em tempo de compilação (convertido em um tipo conhecido).

Usamos o tipo genérico para criar funções, classes ou interfaces que funcionam com diferentes tipos de dados, sem a necessidade de desativar a verificação de tipos.

Quais são os benefícios do tipo genérico em Typescript?

Como dito anteriormente o uso do Tipo Genérico apresenta uma série de benefícios, vejamos:

Reusabilidade: Você pode escrever funções que funcionam com diferentes tipos de dados, evitando a duplicação de código.

Segurança de Tipo: O TypeScript garante que os tipos sejam corretos em tempo de compilação, ajudando a evitar erros.

Flexibilidade: Você pode criar funções que são mais flexíveis e que podem trabalhar com uma variedade de tipos.

Cuidados com Tipos Genéricos

O primeiro cuidado ao se criar tipos genéricos em Typescript, está relacionado com o seu retorno, vejamos um exemplo de um retorno incorreto:

function olaNome<T>(nome: T): string{
 ....
}

Regra: O retorno de uma função generica sempre deve ser um tipo genérico.

Sendo assim, o código acima está incorreto, pois o Typescript gerará um erro alegando que o retorno da função deve ser um tipo genérico e não uma string.

Vejamos a sua correção:

function olaNome<T>(nome: T): T{
 ....
}

Além disso, tipos genéricos não precisam retornar tipos genéricos, como é o caso de uma função genérica que não retorna nada:

function log<T>(valor: T): void {
 console.log(valor);
}

log(123);// Saída: 123
log("texto");// Saída: texto

Tipos Genéricos e seu uso com Arrays

Podemos também criar uma Função Genérica que recebe um array que pode conter diversos tipos de dados diferentes, observe:

function elementos<T>(lista: T[]): void {
 // Percorre o array e exibe cada elemento no console
 lista.forEach((item, index) => {
 console.log(`Índice ${index}: ${item}`);
 });
}

//Podemos definir um array de números:
const numeros: number[] = [1, 2, 3, 4, 5];
elementos(numeros); 

//Ou quem sabe um array de strings:
const textos: string[] = ["Olá", "Mundo", "TypeScript"];
elementos(textos); 

//Ou talvez passar varios valores (any):
elementos([1, "micilini", true, "roll", 98]);

No exemplo acima criamos uma função genérica chamada elementos que pode trabalhar com diversos tipos de arrays de forma genérica, sem se preocupar muito com o tipo que a função está recebendo.

Reduzindo o Escopo das Funções Genéricas com Constraints

Assim como vimos na lição de Union Types, é possível reduzir o escopo dos tipos de dados permitidos dentro das funções genéricas.

Isso faria com que limitássemos os tipos de dados que podem ser utilizados em um Tipo Genérico.

Vejamos como isso funciona:

function soNumeros<T extends number | string>(a: T, b: T): T{
 ....
}

Na sintaxe acima, nós definimos que o tipo genérico T pode assumir somente dois valores, que são number ou string.

Isso significa que qualquer outro tipo de dado como array, objetos, booleanos e afins, não serão aceitos naquela função, ocasionando um erro de compilação.

Essa técnica é conhecida como Constraints, que nada mais é do que uma forma de restringir os tipos de dados que um tipo genérico pode assumir;

Vejamos agora a mesma sintaxe acima com um código mais completo:

function soNumeros<T extends number | string>(a: T, b: T): T {
 if (typeof a === 'number' && typeof b === 'number') {
 // Se ambos os argumentos são números, retornamos a soma
 return (a + b) as T;
 } else if (typeof a === 'string' && typeof b === 'string') {
 // Se ambos os argumentos são strings, retornamos a concatenação
 return (a + b) as T;
 } else {
 // Se os tipos não são compatíveis, lançamos um erro
 throw new Error("Os argumentos devem ser ambos números ou ambos strings.");
 }
}

// Caso com números
const soma = soNumeros(10, 20); // Tipo inferido: number
console.log(soma); // Saída: 30

// Caso com strings
const concatenacao = soNumeros("Olá, ", "Mundo!"); // Tipo inferido: string
console.log(concatenacao); // Saída: Olá, Mundo!

// Caso com tipos mistos (deve lançar um erro)
// const erro = soNumeros(10, "texto"); // Erro: Os argumentos devem ser ambos números ou ambos strings.

Lembre-se de usar o extends informando os tipos de dados que o tipo genérico pode assumir.

Note também que precisamos usar o retorno informando o comando as T, que diz ao compilador que o retorno será um tipo genérico.

"Por que usar o as T, e em quais situações ele deverá ser utilizado?".

O comando as T, significa "Ele é T" ou sendo mais específico "Ele é um Tipo Genérico", que diz ao compilador que o retorno ou uma variável é um tipo genérico.

Ele é bastante utilizado em funções que possuem um retorno genérico, como por exemplo:

function primeiroElemento<T>(arr: T[]): T {
 if (arr.length === 0) {
 throw new Error("O array está vazio.");
 }
 return arr[0] as T; // Usamos 'as T' para garantir que o tipo de retorno é T
}

//Uso da Função:
const numeros = [1, 2, 3, 4, 5];
const primeiroNumero = primeiroElemento(numeros);
console.log(primeiroNumero); // Saída: 1

Note que estamos usando o as T no retorno da função primeiroElemento(), para indicar ao compilador que o nosso array possui um tipo genérico.

Especificando Tipos Durante a Chamada de Funções Genéricas

Antes de entrarmos nesse assunto, gostaria que você prestasse um pouco de atenção na lógica abaixo:

function unirArrays<T>(arr1: T[], arr2: T[]){
 return arr1.concat(arr2);
}

unirArrays([1, 2, 3], [4, 5, 6]);

O código acima é uma função genérica que recebe dois arrays e faz a união de cada um deles em um único array.

Só que... se definirmos strings no segundo array e tentarmos fazer essa união:

function unirArrays<T>(arr1: T[], arr2: T[]){
 return arr1.concat(arr2);
}

unirArrays([1, 2, 3], ["Micilini", "Roll"]);

O compilador vai alegar o seguinte problema: 

Dizendo que: Type 'string' is not assignable to type 'number'.

Mas por que será que isso aconteceu?

Simples, se lembra quando eu falei em tópicos anteriores que a inferência do tipo genérico é definida em tempo de execução?

Então, quando você envia o primeiro grupo de arrays que representam valores numéricos ([1, 2, 3]), o compilador infere que o tipo T é um number, sendo assim, ele espera que o segundo argumento (que também é do tipo T) também seja um number.

O problema é que ele é uma string (["Micilini", "Roll"]).

Atualmente a única forma que nós temos para resolver este problema é definindo um Union Type, logo após a declaração da chamada da nossa função:

function unirArrays<T>(arr1: T[], arr2: T[]){
 return arr1.concat(arr2);
}

unirArrays<number | string>([1, 2, 3], ["Micilini", "Roll"]);

Dessa forma, dizemos ao compilador que estamos enviando numbers e strings.

Agora vamos dar uma olhada neste segundo exemplo:

function unirArrays<T extends number | string>(arr1: T[], arr2: T[]){
 return arr1.concat(arr2);
}

unirArrays([1, 2, 3], ["Micilini", "Roll"]);

Por que o extends não funciona neste caso?

O fato do extends não funcionar, está ligado diretamente a inferência de tipos, pois a partir do momento em que enviamos o primeiro array (que só possui números), ele já infere que ele é do tipo number, ignorando totalmente o fato de que ele pode receber uma string.

Portanto, mesmo o extends podendo receber um number ou string, quando ele recebe primeiramente um number, todos os tipos genéricos devem ser number e ponto final.

Funções com Tipo Unknown

Dentro das nossas funções, nós podemos fazer o uso do tipo unknown (desconhecido), que funciona de forma semelhante ao any, mas com um porém: O compilador não deixa executar esse tipo, caso não houver uma validação do mesmo.

Vejamos como ele funciona na prática:

function olaMundo(nome: unknown){
 console.log('Olá ' + nome);
}

olaMundo('Micilini');

No caso do exemplo acima, estamos recebendo um parâmetro do tipo unknown (desconhecido).

E apesar do código acima funcionar sem nenhum do compilador, as coisas começam a desandar quando usamos tipos mais complexos, como por exemplo:

function olaMundo(nome: unknown){
 console.log('Olá ' + nome[0]);
}

olaMundo('Micilini');

Como o TS não consegue validar o tipo da variável nome, ele mostra a seguinte mensagem de erro: 'nome' is of type 'unknown'.

E para resolver este problema, o ideal é que façamos uma verificação de tipos da seguinte forma:

function olaMundo(nome: unknown){
 if(Array.isArray(nome)){//Verificamos se o nome é um array antes de printar algo no console
 console.log('Olá ' + nome[0]);
 }
}

olaMundo('Micilini');

Apesar disso, a ideia é sempre validar o tipo do parâmetro unknown, seja usando o typeof, instanceOf, ou por meio de comparação direta.

function olaMundo(nome: unknown){
 if(typeof nome === "string"){//Forma mais correta de se utilizar o unknown
 console.log('Olá ' + nome);
 }
}

olaMundo('Micilini');

Podemos perceber que o tipo unknown, funciona como uma especie de trava de segurança que nós impede de executar operações de forma direta sem antes validar o seu tipo.

Funções com o Tipo Never

No Typescript, podemos ter funções que não retornam absolutamente nada, Você se lembra que tipos de funções são essas?

Isso mesmo, são as funções sem retorno do tipo void que vimos em tópicos anteriores desta lição  😁

Só que além do tipo void, nós também temos o tipo never, que funciona de forma semelhante ao void, porém, ele foi criado para retornar apenas ERROS!

Vejamos como ele funciona:

// Função que lança uma exceção e nunca retorna um valor
function throwError(message: string): never {
 throw new Error(message);
}

// Exemplo de uma função que usa uma variável do tipo 'never' para garantir que todos os casos sejam tratados
function assertNever(value: never): never {
 throw new Error(`Unexpected value: ${value}`);
}

Simples, não?

Arquivos da lição

Os arquivos desta lição podem ser encontrados no repositório do GitHub por meio deste link.

Conclusão

Nesta lição você aprendeu um pouco mais sobre o uso de funções no Typescript.

Até a próxima 🤩