Deixe-me explicar. Tenho o seguinte caso de uso: No Angular, estou usando um componente de UI de tabela, que se parece com o seguinte:
<my-table [values]="myTableValues">
<my-column field="firstColumn" />
</my-table>
myTableValues
tem um tipo para suas linhas (vamos chamá-lo de myTableValuesRowType
) que é gerado por meio de um gerador de código swagger. Como você vê, o field
pega uma string como valor, o que o torna menos restrito ao tipo.
Eu criei uma solução que cria um objeto do tipo com as mesmas propriedades e retorna o valor string de cada chave. Então, basicamente, algo assim:
[Key]: KeyString
Este é meu código:
type NonNullableKeys<T> = {
[K in keyof T]: T[K] extends null | undefined ? never : K;
}[keyof T];
export type PropertiesOf<T> = Record<NonNullableKeys<T>, string>;
export function getTypeProperties<T>(): PropertiesOf<T> {
return new Proxy({}, {
get: (_target, prop: string) => prop,
}) as PropertiesOf<T>;
}
Exemplo de playground datilografado
Isso torna mais fácil para nós tornar a propriedade field
no modelo HTML mais restrita ao tipo. Pode não ser a melhor solução, mas é a melhor/mais fácil solução que poderíamos usar agora. Exemplo de uso:
//component.ts
readonly rowProperties = getTypeProperties<myTableValuesRowType>();
//component.html
<my-table [values]="myTableValues">
<my-column [field]="rowProperties.firstColumn" />
</my-table>
Dessa forma, se o tipo gerado alterar as chaves de propriedade, eu conseguiria detectá-lo imediatamente.
No entanto, há um problema principal: a função acima não é recursiva. Então, propriedades aninhadas são ignoradas.
Exemplo:
type myTableValuesRowType = {
firstColumn: number;
secondColumn: string;
thirdColumn: number[];
fourthColumn: boolean;
subrow: {
firstSubrowColumn: string;
secondSubrowColumn: number;
}
}
const rowProperties = getTypeProperties<myTableValuesRowType>();
console.log(rowProperties.firstColumn); //returns "firstColumn"
console.log(rowProperties.subrow.firstSubrowColumn); //returns undefined & TS-Error: Property 'firstSubrowColumn' does not exist on type 'string'. expecting to return "subrow.firstSubrowColumn"
Finalmente, minha pergunta seria: como posso torná-lo recursivo? Tentei várias abordagens e fiz algumas pesquisas para encontrar algo semelhante. Tudo o que encontrei foi obter chaves aninhadas em outro tipo , mas não realmente extrair as propriedades e criar um objeto dele.
Para sua informação, não consigo alterar a Biblioteca de Componentes da IU...
Eu apreciaria qualquer ajuda ou informação!
Atualização: Encontrei algo muito parecido , porém ele converte em uma função e não parece tão bom quanto se fosse um objeto.
Outra atualização: esqueci de mencionar que espero rowProperties.subrow.firstSubrowColumn
retornar subrow.firstSubrowColumn
e não apenas a chave filhafirstSubrowColumn
Tipos como
MyTableValuesRowType
são apagados quando TypeScript é compilado para JavaScript. Isso significa que quando você chamagetTypeProperties<MyTableValuesRowType>()
, a chamada de tempo de execução se parece comgetTypeProperties()
. E se você chamassegetTypeProperties<SomeOtherRowType>()
, também se tornariagetTypeProperties()
em tempo de execução. Portanto, a implementação degetTypeProperties()
não pode saber sobre qual tipo você está perguntando. Qualquer coisa que ele faça precisa ser independente do tipo TypeScript.Isso significa que o que você está pedindo é impossível como declarado. Não há como para
rowProperties.firstColumn
ser umstring
whilerowProperties.subrow
é umobject
. Eles são ambosstring
s ou ambosobject
s. Você terá que: desistir completamente; ou refatorar seu código para que você passe a ele algum tipo de objeto de esquema que exista em tempo de execução, mas represente o formato da interface; ou você pode mudar o comportamento pretendido degetTypeProperties()
para que ele possa se comportar de uma forma independente de tipo, mas seja versátil o suficiente para atingir seus objetivos.Como exemplo do último, vamos fazer com que
rowProperties.firstColumn
erowProperties.subrow
sejam ambos objetos, e se você quiser o nome da chave disso, você indexa esse objeto com uma chave especial. Para não interferir em nenhuma chave existente, eu diria que deveríamos usar umsymbol
:Uma vez que fazemos isso, podemos escrever
getTypeProperties()
para que ele sempre retorne um objeto, um que lhe dá a string desejada quando você indexa nela comkeyName
, e caso contrário lhe dá outro objeto do mesmo tipo. Assim:Aqui adicionei um parâmetro opcional para
getTypeProperties()
que mantém o controle da chave desejada para retornar nakeyName
propriedade. Para uma chamada de nível superior é apenasundefined
, já que não há nome de chave para esse nível. Dentro doProxy
getter ele apenas verifica se a propriedade é um símbolo (você pode fazer com que ele verifiquekeyName
especificamente, mas então você precisa decidir qual string gerar para chaves de símbolo, e isso está fora do escopo aqui). Se for um símbolo você recebe a chave passada. Caso contrário é uma chave de string e nós retornamos o resultado de uma nova chamada paragetTypeProperties()
que anexa o nome da propriedade atual ao caminho existente, com um ponto no meio se o caminho pai não forundefined
.Então o comportamento parece
Seu IDE TypeScript saberá quais propriedades são esperadas que existam (quando implementamos
PropertiesOf<>
abaixo, mas tenha em mente que em tempo de execução, ele realmente não se importa. Na verdade, você pode chamá-lo com nomes de chaves aleatórios e obter um resultado em tempo de execução:O benefício da digitação aqui é tornar a linha acima um erro no TypeScript, em tempo de compilação, mesmo que nada "ruim" aconteça em tempo de execução.
Ok, agora... como podemos implementar
PropertiesOf<T>
? Uma abordagem parece:É um tipo condicional que verifica se
T
é um objeto não array (arrays são objetos em JS/TS, então você tem que verificar). Se for, então ele diz que há umastring
propriedade nakeyName
chave de símbolo, e então mapeia sobre as propriedades deT
para aplicarPropertiesOf
a cada propriedade recursivamente, e cruza esses tipos juntos (então ele tem umakeyName
chave, e todas as chaves existentes também). Caso contrário, se for um array, ou não um objeto, ele tem apenas umastring
propriedade nakeyName
chave de símbolo.Isso funciona, mas produz tipos IntelliSense com muitas interseções aninhadas. Eu geralmente gosto de "embelezar" com algo como:
onde
Id<T>
apenas empacotaT
em um único tipo. Agora, observe que a assinatura de chamada degetTypeProperties()
éo que significa que o tipo de objeto de nível superior retornado não tem uma
keyName
propriedade (novamente, esse é apenas o tipo TypeScript. O comportamento de tempo de execução não sabe nada sobre isso). Então, agora, se olharmos para o tipo dessa chamada:Você pode ver a estrutura recursiva que queríamos. Cada propriedade de
rowProperties
é um objeto com umastring
propriedade com valor - nakeyName
chave. E asubrow
propriedade tem propriedades adicionais emfirstSubrowColumn
esecondSubrowColumn
.Link do playground para o código