typescript - Acessando subobjetos de um objeto sem usar any
Estou no processo de aplicar TypeScript melhor a um projeto. Há um objeto complexo vindo do meu backend que é uma classe com dezenas de atributos, a maioria dos quais são classes próprias. Temos um formulário com um manipulador de mudança comum que atualmente tem alguns anys para fazê-lo funcionar, mas eu gostaria de corrigir isso com generics.
Esta é uma versão simplificada do padrão em meu aplicativo, mas sou capaz de reproduzir os mesmos erros que estou vendo:
class PhoneNumber {
prefix: number;
exchange: number;
line: number;
}
class Address {
address1: string;
address2: string;
city: string;
}
class User {
phone: PhoneNumber;
address: Address;
}
const user = new User();
function setValue<A extends keyof User, T extends User[A], K extends keyof T, V extends T[K]>(
value: V,
fieldType: new () => T,
fieldName: A,
fieldAttribute: K
) {
if (!user[fieldName]) {
user[fieldName] = new fieldType();
}
const field = user[fieldName];
// error here.
field[fieldAttribute] = value;
}
setValue(408, PhoneNumber, 'phone', 'prefix');
Tentei algumas coisas, e isso é o mais próximo que consegui chegar de uma solução. Consigo inspecionar a chamada da função no final no meu IDE, e parece que os genéricos estão sendo preenchidos com algo razoável: function setValue<"phone", PhoneNumber, "prefix", number>(), mas então recebo um erro durante a compilação:
TS2536: O tipo 'K' não pode ser usado para indexar o tipo 'User[A]'.
Estou fazendo isso da maneira errada? Se eu não conseguir resolver isso, vou acabar apenas separando os diferentes tipos em manipuladores distintos.
Também há um caso em que alguns dos campos são arrays de objetos, o que adiciona outra complicação a toda a situação.
Resposta #1
Com os tipos de argumento atuais de setValue, alguém poderia fornecer uma classe child B como fieldType (que extende a classe A user[fieldName]), e direcionar um fieldAttribute de B, que pode não existir em A:
class PhoneNumber2 extends PhoneNumber {
extension: number;
}
// Complies with setValue types,
// but if `user` already has a basic PhoneNumber instance,
// the code within setValue fails
setValue(408, PhoneNumber2, "phone", "extension");
// within setValue:
user["phone"]["extension"] = 408;
// ...but user["phone"] may be a PhoneNumber,
// not necessarily a PhoneNumber2!
No seu caso, em vez de K extends keyof T (T sendo o tipo de retorno de fieldType, potencialmente uma classe filha), você poderia apenas usar K extends keyof User[A], para garantir que o fieldAttribute aponte para a classe base:
function setValue2<A extends keyof User, T extends User[A], K extends keyof User[A], V extends T[K]>(
value: V,
fieldType: new () => T,
fieldName: A,
fieldAttribute: K
) {
if (!user[fieldName]) {
user[fieldName] = new fieldType();
}
const field = user[fieldName];
field[fieldAttribute] = value; // Now okay
}
setValue2(408, PhoneNumber, 'phone', 'prefix');
setValue2(408, PhoneNumber2, 'phone', 'prefix');
setValue2(408, PhoneNumber2, 'phone', 'extension'); // Error: Argument of type '"extension"' is not assignable to parameter of type 'keyof PhoneNumber'.
Você poderia até se livrar do tipo genérico T:
function setValue3<A extends keyof User, K extends keyof User[A], V extends User[A][K]>(
value: V,
fieldType: new () => User[A],
fieldName: A,
fieldAttribute: K
) {}
Link do Playground
