Estou tentando construir um tipo recursivo em TypeScript que resolva cada caminho de rota possível, incluindo caminhos aninhados, a partir de uma determinada configuração de rota. No entanto, estou encontrando problemas com restrições de tipo e inferência.
Eu defini as rotas usando uma interface TypedRoute e uma função createRoute:
interface TypedRoute<
Path extends string,
QueryParams extends Record<string, string> = {}
> {
path: Path;
queryParams?: QueryParams;
children?: TypedRoute<any, any>[];
}
function createRoute<
Path extends string,
QueryParams extends Record<string, string> = {}
>(config: TypedRoute<Path, QueryParams>) {
return config;
}
const routes = [
createRoute({
path: 'results/:id/foo/:bar'
}),
createRoute({
path: 'home',
children: [
createRoute({
path: 'bar',
children: [
createRoute({ path: 'baz' })
]
}),
],
}),
]
Agora, estou tentando criar um tipo ExtractRoutePaths que extraia recursivamente todos os caminhos possíveis:
type ExtractRoutePaths<T extends readonly TypedRoute<any, any>[]> =
T extends readonly []
? never
: T extends readonly [infer First, ...infer Rest]
? First extends TypedRoute<infer P, any>
? First extends { children: infer C }
? C extends readonly TypedRoute<any, any>[]
? `{P}` | ExtractRoutePaths<C, `${P}/`> | ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
: `${P}` | ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
: `${P}` | ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
: ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
: never;
Espero que as rotas sejam:
resultados/:id/foo/:bar | casa | início/bar | casa/bar/baz
O que estou fazendo de errado e como posso construir corretamente esse tipo recursivo para extrair todos os caminhos possíveis, incluindo os aninhados?
A seguir, estou ignorando as
queryParams
coisas que não parecem ser diretamente relevantes. Você pode adicioná-lo novamente aos seus próprios tipos, masExtractRoutePaths<T>
não é necessário. Na verdade, vou apenas olhar para o tipoque é tudo que você precisa para extrair os caminhos. Escreverei
ExtractRoutePaths<T>
para operar em uma única rota; se você precisar operar em uma série de rotas, você pode escreverOk, então aqui está
ExtractRoutePaths<T>
:Este é um tipo condicional recursivo e bastante simples. Sabemos que sempre incluirá
T["path"]
(o tipo que você obtém quando indexa umT
valor com apath
chave de propriedade). Então pegamos isso e colocamos em união com a etapa recursiva. Usamos e em um tipo condicional para verificar se realmente possui uma propriedade do tipo esperado (já que é opcional, podeinfer
extends
não existir ou pode existir ) e armazenamos essa propriedade na variável de tipo . Caso contrário, não precisamos recorrer e temos nosso caso base (que é apenas ). Nesse caso, retornamos um tipo literal de modelo que une o caminho atual com a união de todos os caminhos filhos, com uma barra no meio.T
children
undefined
C
never
Observe que o tipo condicional aqui é distributivo sobre os tipos de união, assim como os acessos indexados e os tipos literais de modelo; portanto, o tipo único acima coletará naturalmente todos os caminhos diferentes em uma única união.
Para testá-lo, devemos fornecer um tipo de rota. Mas antes de podermos fazer isso, precisamos modificar o seu
createRoute()
para que ele acompanhe exatamente o que você dá a ele. Não queremos apenas inferir o tipo de caminho principal, que é o que sua versão faz. É melhor torná-lo genérico no tipo doconfig
parâmetro inteiro, assim:E observe o
const
parâmetro type que pede ao TypeScript para inferir tipos literais estreitos para valores de entrada literais, em vez de inferir tipos mais amplos comostring
.Então agora podemos testar:
Parece bom. Você pode ver que
routes
possui informações de tipo suficientes para representar toda a estrutura da árvore eExtractRouteArrayPaths
(e, portantoExtractRoutePaths
) extrai-as como uma união de caminhos correspondentes.Observe que tipos profundamente recursivos como esse tendem a ter casos extremos bizarros e, às vezes, exigem refatoração significativa para lidar com eles. Portanto, é importante testar qualquer tipo desse tipo em uma ampla variedade de casos de uso representativos e estar preparado para mantê-lo mais do que você gostaria diante de casos de uso futuros ou até mesmo de alterações de idioma.
Link do Playground para o código