Tip generic TypeScript cu nume de chei modificate dintr-un alt tip


Să zicem că am o interfață

interface Foo {
   id: string;
   bar: string;
}

interface Person {
   name: string;
   age: number;
   birthdate: Date;
   active: boolean;
   foo: Foo;
}

Vreau să pot crea o interfață cu numele primei interfețe, dar concatenate cu un alt șir. Deci, dat fiind tipul Generic FilterFor, pot să creez un tip generic

type FilterFor<T> = ...

Astfel încât apelarea FilterFor<Person> ar produce următoarea interfață:

interface FilterForPerson {
    name: string;
    name__in: string[];
    name__search: string;
    age: number;
    age__gt: number;
    age__gte: number;
    age__lt: number;
    age__lte: number;
    birthdate: Date;
    birthdate__gt: Date;
    birthdate__gte: Date;
    birthdate__lt: Date;
    birthdate__lte: Date;
    foo: string|Foo
    foo__in: Array<string|Foo>
    active: boolean
}

editare: Am adăugat câmpurile date, foo, active

Răspuns #1

Există diverse moduri posibile de a implementa acest lucru. Soluția pe care o prezint aici implică remaparea cheilor în tipuri mapate, ceea ce vă permite să transformați atât cheile, cât și valorile tipului dvs. de intrare. Mai întâi vom calcula un tip intermediar numit Fields<T> care transformă tipul dvs. de intrare T într-o mare uniune de tipuri tuple cheie-valoare, iar apoi vom crea FilterFor<T> peste rezultatele obținute pentru a obține tipul final. (Mulțumiri lui @Oblosys pentru această abordare).

Deci Fields<{a: string, b: number}> ar fi ceva de genul ["a", string] | ["a__search", string] | ["a__in", string[]] | ["b", number] | ["b__gt", number] | ⋯, iar apoi FilterFor<{a: string, b: number}> ar parcurge Fields<{a: string, b: number}> pentru a obține {a: string, a__search: string, a__in: string[], b: number, b__gt: number, ⋯ }.


Iată cum aș scrie Fields<T>:

type Fields<T> = { [K in string & keyof T]:
  T[K] extends number | Date ? [K | `${K}__${"gt" | "gte" | "lt" | "lte"}`, T[K]] :
  T[K] extends object ? [K, string | T[K]] | [`${K}__in`, (string | T[K])[]] :
  T[K] extends string ? [K | `${K}__search`, string] | [`${K}__in`, string[]] :
  [K, T[K]]
}[string & keyof T]

Acesta este un tip de obiect distributiv de forma {[K in KK]: F<K>}[KK], care ajunge să calculeze o reuniune de F<K> pentru fiecare K din reuniunea KK. În exemplul de mai sus, KK este string & keyof T, sau doar cheile de tip string ale tipului de intrare T (aceasta exclude cheile de tip number și cheile de tip symbol, dacă există). Și F<K> este o serie de tipuri condiționale care creează tuplurile corecte cheie-valoare pentru K.

Regulile particulare pe care le urmez sunt... mai întâi verifică tipul valorii T[K] al proprietății lui T la cheia K.

  • Dacă este number sau Date, atunci vrem un set de chei care include K și o concatenare de tip șablon-literal a K cu diferitele sufixe de comparație, iar tipul valorii rămâne neschimbat.

  • Altfel, dacă este un obiect (și rețineți că Date este un obiect, deci trebuie să procedăm în această ordine), atunci dorim să păstrăm cheia K și valoarea T[K] unite cu string, și de asemenea dorim cheia K cu sufixul __in și o valoare care să fie un array de T[K]-unit-cu-string.

  • Altfel, dacă este string, atunci dorim ca K și K-cu-sufix-__search să aibă o valoare string, iar K-cu-sufix-__in să aibă o valoare string[].

  • Altfel, pur și simplu lăsați cheia K și valoarea T[K] așa cum sunt.

Odată ce facem asta, FilterFor<T> este pur și simplu:

type FilterFor<T> = { [F in Fields<T> as F[0]]: F[1] }

care pur și simplu parcurge fiecare intrare în Field<T> folosind primul element ca cheie și al doilea element ca valoare.


Bine, să îl testăm:

interface Foo {
  id: string;
  bar: string;
}

interface Person {
  name: string;
  age: number;
  birthdate: Date;
  active: boolean;
  foo: Foo;
}

type FilterForPerson = FilterFor<Person>
/* type FilterForPerson = {
    name: string;
    name__search: string;
    name__in: string[];
    age: number;
    age__gt: number;
    age__gte: number;
    age__lt: number;
    age__lte: number;
    birthdate: Date;
    birthdate__gt: Date;
    birthdate__gte: Date;
    birthdate__lt: Date;
    birthdate__lte: Date;
    active: boolean;
    foo: string | Foo;
    foo__in: (string | Foo)[];
} */

Arată bine! Proprietățile age și birthdate primesc tratamentul number-sau-Date, în timp ce proprietatea foo primește tratamentul non-Date-object, proprietatea name primește tratamentul string, iar active primește tratamentul pentru tot ce rămâne.

Link de teren de joacă către cod