A Cassidy Williams tem uma excelente newsletter, que sempre inclui uma “Pergunta de entrevista da semana”. A pergunta dessa semana era:
Dado um array de strings que representa uma sequência de estados de semáforo, escreva uma função que verifique se a sequência pode representar estados válidos para um semáforo padrão. As únicas transições válidas são: de vermelho para verde, de verde para amarelo e de amarelo para vermelho.
Para deixar claro, alguns exemplos:
> isValidTrafficSequence(["red", "green", "yellow", "red", "green"])
> true
> isValidTrafficSequence(["red", "yellow", "green"]);
> false
> // Inválido porque "yellow" não pode vir depois de "red",
> // nem "green" depois de "yellow"
> isValidTrafficSequence([])
> true
Parece um desafio interessante, mas eu tive uma ideia meio maluca, “Será que é possível fazer essa validação utilizando Typescript?”. E eu não estou falando de código runtime, eu estou falando de verificar, utilizando somente o sistema de types, se é uma sequência válida durante o processo de compilação.
Depois de bater um pouco a cabeça, eu cheguei na seguinte solução:
type Color = "red" | "yellow" | "green";
type ValidPairs = ["red", "green"] | ["green", "yellow"] | ["yellow", "red"];
type ValidSequence<T extends readonly Color[]> =
T extends readonly [] ? true :
T extends readonly [Color] ? true :
T extends readonly [infer First, ...infer Rest] ?
[First, Rest[0]] extends ValidPairs ?
Rest extends readonly Color[] ?
ValidSequence<Rest>
: false
: false
: false;
type Test1 = ValidSequence<["red", "green", "yellow", "red"]>;
//^? type Test1 = true
type Test2 = ValidSequence<["red", "yellow", "green"]>;
//^? type Test2 = false
type Test3 = ValidSequence<[]>;
//^? type Test3 = true
Link para a solução e testes no Playground
A lógica é relativamente simples, depois que você entende a sintaxe do Typescript.
A keyword extends
antes da atribuição (simbolo de =
) corresponde a uma restrição do genérico. No nosso caso, significa que ValidSequence
recebe um parametro T
que precisa ser do tipo readonly Color[]
. Mas o extends
depois de atribuição permite uma verificação com um operador ternário. Por exemplo, podemos fazer:
type IsString<T> = T extends string ? true : false;
type Name = IsString<"Renato">
//^? type Name = true
type Age = IsString<33>
//^? type Age = false
O legal é que dentro desse operador temos acesso à keyword infer
, que permite criar uma variável temporária que corresponde ao valor verificado. Em outras palavras, infer
diz: “Se o tipo corresponder a esse formato, capture ele nesta variável”.
type ColorString<T> = T extends `cor-${infer Cor}` ? Cor : never;
type JustColor = ColorString<"cor-vermelha">
//^? JustColor = "vermelha"
Nesse exemplo, se T
é um string que tem o formato cor-algo
, ele vai extrair esse algo
em uma variável chamada Cor
.
Na nossa solução, usamos infer First
para extrair o primeiro elemento e ...infer Rest
para extrair o restante da lista, utilizando a sintaxe de espalhamento ("spread").
Agora podemos entender melhor a lógica da solução do desafio:
type ValidSequence<T extends readonly Color[]> =
// É uma lista vazia?
T extends readonly [] ? true :
// É uma lista com somente uma cor?
T extends readonly [Color] ? true :
// Salva o primeiro elemento em `First` e o resto em `Rest`
T extends readonly [infer First, ...infer Rest] ?
// É um par válido?
[First, Rest[0]] extends ValidPairs ?
// O resto da lista é o formato esperado (aqui sempre é, mas o typescript não sabe)
Rest extends readonly Color[] ?
// Recursivamente, faça a mesma validação para o resto da lista
ValidSequence<Rest>
: false
: false
: false
O equivalente em runtime
seria:
function isValidSequence(list) {
return list.length === 0 ? true :
list.length === 1 && isColor(list[0]) ? true :
// esse const aqui não é válido, mas imagina que fosse possível
const [first, ...rest] = list;
isValidPair(first, rest[0]) ?
isValidSequence(rest) :
false
false
false
}
// Ou, de formas mais legível:
function isValidSequence(list) {
if (list.length === 0) return true;
if (list.length === 1 && isColor(list[0])) return true;
const [first, ...rest] = list;
if (isValidPair(first, rest[0])) {
isValidSequence(rest)
}
return false;
}
Você deve ter notado que foi precisa utilizar o readonly
em vários pontos. Isso acontece porque arrays são mutaveis no Javascript, então o Typescript não teria certeza que os elementos iriam se manter na mesma ordem. Para deixar claro para o Typescript que aquele array não vai ser mutato, você pode adicionar um as const
.
const mutableSequence = ["red", "green", "yellow"];
//^? string[]
const immutableSequence = ["red", "green", "yellow"] as const;
//^? readonly ["red", "green", "yellow"]
Esse exercício demonstra a capacidade do sistema de tipos do Typescript. Lógico que nem sempre vale a pena escrever esses tipos complexos, principalmente porque os erros gerados são muito difíceis de entender e o código tem uma legibilidade muito baixa, mas é interessante ver até onde conseguimos chegar apenas com o sistema de tipos.