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.
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 a nível de tipo (type-level).
infer
Para que isso possa ser 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.
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.
1 2 3 4
const x = [0]; if (x[0] === 0) { // aqui você sabe que o `x[0]` sempre vai ser 0 }
E o infer
?
1
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.
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?
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.
1 2 3 4 5 6
export type tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...tuple<Rest>] : [] export type length<S extends string> = tuple<S>["length"] type Len = 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
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).I
e os posteriores sendo uma
recursão do próprio tipo tuple.length
, que
tem armazenado o tamanho do 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
1 2 3 4
export type Tuple<S extends string> = S extends `${infer I}${infer Rest}` ? [I, ...Tuple<Rest>] : [] export type Length<S extends string> = Tuple<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.
Nome complicado para os tipos builtins que manipulam string, sendo eles
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.
1 2
type CaixaAlta = Uppercase<"string"> type CaixaBaixa = LowerCase<"STRING">
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.