Preciso fazer referência ao typeof de um objeto dentro do próprio objeto, assim:
type Config<T extends Record<string, Config<T>> = any> = {
name: string,
value: number,
children?: { [key: string]: Config<T> }
visibilityFn?: VisibilityFn<T>
}
type VisibilityFn<T extends Record<string, Config>>= (children: T) => boolean
const config = {
name: 'config',
value: 1,
children: {
one: {
name: 'one',
value: 1,
visibilityFn: (children: typeof config.children) => children.two.value > 3
},
two: {
name: 'two',
value: 2,
},
},
} satisfies Config
Não quero digitar explicitamente a forma dos filhos por meio de um tipo genérico, porque com estruturas mais complicadas e múltiplas reutilizações isso vai ficar velho muito em breve. Eu poderia simplesmente não digitar a função, mas obviamente não quero isso. A última opção que pensei é definir as funções de visibilidade fora da configuração, mas com estrutura aninhada defini-la para cada elemento é trabalhoso. Alguma ideia de como torná-la razoavelmente breve e estritamente tipada?
Você pode criar um auxiliar de tipo recursivo que permite obter essa inferência de tipo dinamicamente.
Geralmente, você não pode se referir a
typeof x
dentro da definição para o valorx
sem entrar em conflito com os avisos de circularidade do TypeScript. Na verdade, o que você precisa fazer é darConfig<T>
uma definição genérica recursiva adequada para que ela possa realmente representar o tipo que você precisa automaticamente pretende transmitir, onde em cada nível da árvore, ovisibilityFn
parâmetro de retorno de chamada do método opcional corresponde ao tipo dachildren
propriedade do pai daquele nível. Algo assim:Aqui
TP
está o tipo pai, que para o nível superior deve ser apenas o objeto de tipo vazio (é por isso que escolhi isso como o argumento de tipo padrão paraTP
). Então, sechildren
for do tipoConfigChildren<T>
, entãovisibilityFn
recebe um parâmetro do tipoConfigChildren<TP>
. EntãoConfigChildren
é apenas um tipo mapeado onde cada propriedade estáConfig<T[K], T>
(já que no próximo nível abaixo,T
se torna o pai daT[K]
propriedade).Agora você não pode usar o
satisfies
operador diretamente porque não pode facilmente nomear o tipoconfig
que ele deve satisfazer sem se referir a si mesmo. Ou seja, você precisaria escreversatisfies Config<{one: any, two: { three: any} }>
ou algo assim, e principalmente anula o propósito do que você está fazendo. Se você estivesse feliz escrevendo esse tipo de propriedade como um argumento de tipo, então você teria apenas anotado seu tipo de parâmetro explicitamente em vez de tentar escrevertypeof config.children
.Então, vamos abandonar isso
satisfies
em favor de uma função auxiliar genérica que, com sorte, infere esse tipo de argumento para você:E agora você pode chamá-lo em seu literal de objeto:
Aqui, o argumento de tipo
T
é inferido como{one: unknown; two: {three: unknown}}
, e, portanto, dentro deconfig.one.visibilityFn
, ochildren
parâmetro é contextualmente tipado comoConfigChildren<T>
para issoT
, e, portanto, sabe-se quechildren.two.children
, se existir, tem umathree
propriedade. (Observe que a parte "se existir" é porquechildren
é definido como uma propriedade opcional deconfig
. Se você precisar de algo mais específico para que o TypeScript saiba quando ele aparece e não aparece, você precisará fazer uma refatoração. Mas os detalhes de como esse tipo recursivo deve ser ajustado para seu caso de uso estão fora do escopo aqui.)Isso funciona até certo ponto, mas esteja ciente de que o TypeScript não tem a capacidade de inferir tipos arbitrariamente complicados. Pode ser que em alguma profundidade da sua árvore você precise envolver algo em outra
asConfig()
chamada para que os níveis mais baixos sejam inferidos separadamente dos superiores. Ou você pode se ver tendo que anotar parâmetros de tipo que você realmente prefere que o TypeScript infira para você. Isso pode acontecer, especialmente quando genéricos e tipagem contextual precisam ocorrer simultaneamente ou em alguma ordem específica. Há uma solicitação de recurso em microsoft/TypeScript#47599 para melhorar esse tipo de inferência, mas sempre haverá casos em que isso simplesmente não pode acontecer. Se você se deparar com isso, será muito melhor apenas escrever o tipo de que precisa em vez de tentar forçar o TypeScript a fazer coisas que ele não consegue fazer bem.Link do playground para o código