typescript - Accesarea sub-obiectelor unui obiect fără a folosi any


Sunt în procesul de aplicare a unui TypeScript mai bun într-un proiect. Există un obiect complex venind de la backend-ul meu, care este o clasă cu zeci de atribute, majoritatea fiind clase proprii. Avem un formular cu un handler de schimbare comun care în prezent are câteva any-uri pentru a funcționa, dar mi-ar plăcea să rezolv asta folosind generice.

Aceasta este o versiune simplificată a modelului din aplicația mea, dar sunt capabil să reproduc aceleași erori pe care le văd:

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');

Am încercat câteva lucruri și asta este cât de aproape am reușit să ajung de o soluție. Pot să inspectez apelul funcției la final în IDE-ul meu și se pare că genericele sunt completate cu ceva rezonabil: function setValue<"phone", PhoneNumber, "prefix", number>(), dar apoi primesc o eroare în timpul compilării:

TS2536: Tipul 'K' nu poate fi folosit pentru a indexa tipul 'User[A]'.

Mă abordez greșit această problemă? Dacă nu reușesc să înțeleg asta, o să ajung să împart pur și simplu diferitele tipuri în gestionatori separați.

Există, de asemenea, un caz în care unele dintre câmpuri sunt tablouri de obiecte, ceea ce adaugă o altă complicație întregii situații.

Răspuns #1

Cu tipurile de argumente actuale ale setValue, s-ar putea furniza o clasă child B ca fieldType (care extinde clasa A user[fieldName]), și să se țintească un fieldAttribute al lui B, care s-ar putea să nu existe în 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!

În cazul tău, în loc de K extends keyof T (T fiind tipul de întoarcere al fieldType, posibil o clasă copil), ai putea folosi doar K extends keyof User[A], pentru a te asigura că fieldAttribute vizează clasa de bază:

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'.

Ai putea chiar să scapi de tipul generic 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 către terenul de joacă