Cum să scrii un generic TypeScript astfel încât nicio proprietate SubType să nu fie mai restricționată


Vreau să creez o funcție tipizată generic care returnează un obiect parțial al unui tip. În acest caz, în mod specific, una care atribuie un parametru ID și returnează un parțial. Având un tip concret, pot folosi un model ca acesta fără probleme:

type Person = {
  id: string;
  name: string;
}

const generatePartialPerson = (): Partial<Person> => {
  const id = crypto.randomUUID() as string;
  const obj: Partial<Person> = { id };

  return obj;
}

Încercând să scriu o formă generică care poate accepta orice Tip care are o proprietate {id: string}, soluția pe care am scris-o întâmpină eroarea de tip:

Tipul '{ id: string; }' nu poate fi atribuit tipului 'Partial'

const generatePartialGeneric = <T extends { id: string }>(
): Partial<T> => {
  const id = crypto.randomUUID() as string;
  const obj: Partial<T> = { id };

  return obj;
};

Înțelegerea mea este că acest lucru se datorează faptului că un tip care extinde {id: string} poate să îl restricționeze mai mult, de exemplu, setându-l la un literal de șir fix, așa cum este:

interface FixedPerson extends Person {
  id: "81a245c8-691e-471b-b07f-35e3ea3257ae";
}

Există o modalitate de a scrie o constrângere generică care să afirme că tipul/subtipul trebuie să aibă o proprietate {id: string} care nu a fost restricționată în continuare?

Răspuns #1

Constrângerile generice din TypeScript nu suportă în prezent niciun fel de limită inferioară, așa cum s-a cerut în microsoft/TypeScript#14520. Deci poți spune T extends X pentru a însemna că T trebuie să fie un subtip al lui X (deci X este o limită superioară a lui T), dar nu poți scrie T super X pentru a însemna că T trebuie să fie un supertip al lui X (deci X este o limită inferioară a lui T). Limitele inferioare sunt utile atunci când încerci să reprezinți în siguranță scrierile proprietăților în loc de citirile.

Dacă T super X ar exista în TypeScript, atunci ai putea poate să scrii funcția ta astfel

declare const generatePartialGeneric: 
  <T extends {id: S}, S super string>() => Partial<T>;

dar deocamdată nu face parte din limbă.


Din fericire, poți simula o astfel de constrângere cu tipuri condiționale, așa cum este arătat aici:

const generatePartialGeneric =
  <T extends { id: string extends T["id"] ? unknown : never }>(
  ): Partial<T> => {
    const id: string = crypto.randomUUID();
    const obj = { id } as Partial<T>;
    return obj;
  };

Acest lucru va ajunge să compare T cu el însuși și va funcționa dacă și numai dacă are o proprietate id care este un supertip al string. Așa:

type Person = {
  id: string;
  name: string;
}
const x = generatePartialGeneric<Person>(); // okay

interface Oops {
  id: "oops"
  abc: string;
}
const y = generatePartialGeneric<Oops>(); // error

interface Okay {
  id: string | number
  abc: string;
}
const z = generatePartialGeneric<Okay>(); // okay

Rețineți că în implementare a trebuit să folosesc o afirmație de tip la const obj={ id } as Partial<T> în loc să o notez ca const obj: Partial<T>={ id }.

Asta se întâmplă deoarece limita inferioară simulată depășește cu mult capacitatea compilatorului de a analiza în interiorul implementării funcției generice. Compilatorul pur și simplu nu are nicio idee ce ar putea sau nu fi atribuit la Partial<T>, așa că se plânge dacă încercăm. Și acea plângere este aceeași eroare ca cea din codul tău original. Motivul pentru care există eroarea este din cauza posibilității ca string să fie prea larg pentru proprietatea id, așa cum ai menționat și așa cum este descris în "De ce nu pot să returnez un 'T' generic pentru a satisface un Partial<T>?" ... dar chiar dacă creezi funcția în mod specific astfel încât această posibilitate să nu existe, compilatorul pur și simplu nu o poate vedea. Este prea abstract.

Așa că folosim o afirmare de tip și mergem mai departe.


Există și alte abordări pentru genul de lucru pe care îl faci; una este să nu încerci deloc să capturezi problema id în T, și în schimb să faci ca generatePartialGeneric să returneze o valoare a cărei tip este legat de T. Știm că valoarea returnată va fi de tip {id: string} precum și ceva de genul Partial<Omit<T, "id">>. Adică, indiferent ce proprietate id are T, rezultatul va fi de tip string. Acesta ar putea fi scris astfel:

const generatePartialGeneric = <T extends object>() => {
  const id = crypto.randomUUID() as string;
  const obj: { [K in keyof T as Exclude<K, "id">]?: T[K] } = {};
  return Object.assign(obj, { id });
};

type Person = {
  id: string;
  name: string;
}

const p = generatePartialGeneric<Person>();
/* const p: { name?: string | undefined; } & { id: string; } */

const q: Partial<Person> = generatePartialGeneric<Person>();

Nu există deloc probleme cu constrângerile aici, pentru că nu ne facem griji cu privire la ceea ce T ar putea sau nu ar putea face cu id. Ai putea apela generatePartialGeneric<{a: number}> și ai obține un {a?: number; id: string} rezultat.

Link de teren de joacă către cod