E se? Strings
E se strings fossem tipadas no Typescript?
Cá estamos para falar de Typescript e fazer um experimento. Caso esteja ansioso para o resultado, fique tranquilo, acesse o playground typescript e se divirta.
E se strings fosse tipadas?
Como assim strings tipadas? Quando digo strings tipadas, quero dizer o Typescript inferindo literalmente o valor da string com o seu valor.
Hoje quando trabalhamos com uma string, o nosso retorno sempre é string
e nós nunca sabemos o valor que os métodos irão retornar, uma vez que eles são "empacotados" no tipo string
.
Mas isso pode mudar se a gente se esforçar um pouco para fazer uma tipagem que resolva exatamente aquilo que o Typescript faz em runtime, mas dessa vez ao nível de tipo (type-level).
Trabalhando com o infer
Para que isso seja possível, precisamos entender o conceito de infer
no Typescript. Ele pode ser um pouco complicado de começo, mas vou tentar exemplificar para ficar fácil a sua forma de usar. Antes de tudo, você precisa obedecer algumas regras para o bom uso do infer.
- O infer sempre deve ser usado no extends de um tipo
- O extends sempre deve estar no retorno do tipo
Ok, dadas as duas regras em mente, vamos tentar entender o que o infer
faz. Ele literalmente infere um tipo para você dada uma condição de tipo. Imagine o infer como Eu testei esse valor no if, então sei que a partir de agora, dentro do if, o meu valor sempre vai ser o resultado do teste.
const const x: number[]
x = [0];
if (const x: number[]
x[0] === 0) {
// aqui você sabe que o `x[0]` sempre vai ser 0
}
E o infer
?
type ArrayValue<T extends any[]> T extends Array<infer V> ? V : never;
Como dito anteriormente, o infer
deve ser usado no extends e somente no retorno do tipo. Dito e feito. Mas vale lembrar que extends
no Typescript precisam ser feitos utilizando ternários. Você pode tentar ler o infer
como um pedido ao Typescript, mais ou menos assim...
Typescript, eu não sei o que é tipo
T extends any[]
, mas você pode inferir para mim se o meu tipoT
extenderArray<qualquer coisa>
? Se você puder, retornequalquer coisa
. Caso não, retorne never
Tendo em mente a conversa com o Typescript, agora você pode utilizar o infer
de forma mais consciente, como se você estivesse conversando com o compilador do Typescript.
String literals
No Typescript nós podemos trabalhar com valores literais através de tipo, sendo possível você saber o valor de uma variável apenas olhando para seu tipo e não para o valor em runtime.
Você consegue observar esse comportamento quando você cria uma const
de um valor que seja string. Vale observar que o valor da variável entendido pelo Typescript não é string
, mas sim o valor que você definiu.
Trabalhando com String literals, podemos criar métodos de string de forma poderosa, sendo possível até mesmo dizer o tamanho de uma string em type-level. Como isso é possível?
Literals + Recursividade + infer
Aqui vai começar a sopa de letrinha para criarmos nosso novo tipo string. Para conseguir fazer isso, vamos criar nosso primeiro tipo, o tipo length
, que é responsável por retornar o tamanho da string.
export type type tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...tuple<Rest>] : []
tuple<function (type parameter) S in type tuple<S extends string>
S extends string> = function (type parameter) S in type tuple<S extends string>
S extends `${infer function (type parameter) I
I}${infer function (type parameter) Rest
Rest}` ?
[function (type parameter) I
I, ...type tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...tuple<Rest>] : []
tuple<function (type parameter) Rest
Rest>] : []
export type type length<S extends string> = tuple<S>["length"]
length<function (type parameter) S in type length<S extends string>
S extends string> = type tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...tuple<Rest>] : []
tuple<function (type parameter) S in type length<S extends string>
S>["length"]
type type Len = 10
Len = type length<S extends string> = tuple<S>["length"]
length<"Typescript"> // 10
Caso você jogue esse código no playground do Typescript, você irá ver que o tipo de Len
será 10, que é exatamente o número de caracteres existentes em Typescript
.
Como isso foi possível? Bom, vamos com um passo de cada vez para entender os conceitos do tipo tuple
. Esse tipo recebe um parâmetro chamado S
e através dele começamos a recusão. Lendo parte por parte, temos a seguinte narrativa
- Se o tipo
S
extende o${inferência do tipo I}${inferência do tipo Rest}
. Esse padrão de dois infer dentro de uma string faz com que o compilador entenda que você está se referindo ao primeiro caracter da string(I) e aos posteriores até o último (Rest). - Se a condição anterior for verdade, retorne um Array, sendo o primeiro item o tipo
I
e os posteriores sendo uma recursão do próprio tipo tuple. - Sendo a parte de início da recursão, esse trecho irá se repetir até que o Typescript consiga entender que não há nenhuma string.
- Com essa recursão, teremos uma lista com cada um dos caracteres da string. Daí é só pegar a propriedade
length
, que tem armazenado o tamanho do array.
Contando caracteres com array
Uma das formas de se obter o tamanho de uma string em type-level é por meio de arrays. Basicamente nós precisamos iterar sobre a string, pegando caracter a caracter e adicionando em um array, basicamente um .reduce
, acumulando um valor até não ter mais nenhum caracter iterável na string
export type type Tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...Tuple<Rest>] : []
Tuple<function (type parameter) S in type Tuple<S extends string>
S extends string> = function (type parameter) S in type Tuple<S extends string>
S extends `${infer function (type parameter) I
I}${infer function (type parameter) Rest
Rest}` ?
[function (type parameter) I
I, ...type Tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...Tuple<Rest>] : []
Tuple<function (type parameter) Rest
Rest>] : []
export type type Length<S extends string> = Tuple<S>["length"]
Length<function (type parameter) S in type Length<S extends string>
S extends string> = type Tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...Tuple<Rest>] : []
Tuple<function (type parameter) S in type Length<S extends string>
S>["length"]
No tipo Tuple
, nós iteramos a string utilizando extends
e infer
. Essa jogada de infer I
e infer Rest
pode ser entendida como pegue o primeiro caracter da string e faça inferência do resto da string. Notamos também um tipo recursivo que irá sempre chamar Rest
, ou seja, sempre iremos fazer o nosso tipo Tuple
chamar o restante da string após mesclar o primeiro caracter com os demais da chamada recursiva.
Por fim, no Length
nós apenas chamamos a Tuple
, para nos retornar a lista com todos os caracteres e adicionamos a chamada de ["length]
para obter o tamanho do array e que por sua vez é o tamanho da string.
Intrisic String Manipulation
Nome complicado para os tipos builtins que manipulam string, sendo eles
- Uppercase
- Lowercase
- Capitalize
- Uncapitalize
Esses tipos são bastante pois eles evitam o nosso trabalho de mapear todas as possíveis strings de caixa baixa (lower case) para caixa alta (upper case). E funcionam de maneira muito simples, bastando apenas você utilizar o tipo utilitário e ele será responsável por fazer a conversão.
Tendo conhecimento desses tipos, fazer os métodos toUpperCase
e toLowerCase
fica bem fácil.
type CaixaAlta = Uppercase<"string">
type CaixaBaixa = LowerCase<"STRING">
Conclusão
Com todos esses conceitos apresentados, agora fica bem mais tranquilo de fazer uma implementação de cada um dos tipos de string. Como você pode ver no começo do artigo, temos o link para o playgroud onde você pode acompanhar todas as tipagens feitas para implementar alguns dos métodos de String
. Espero que tenha gostado dessa experiência com tipos, caso não tenha entendido algum tópico é só deixar um comentário e a gente discute sobre. Até a próxima.