Typescript-typebeskytter for å kreve alle elementer


Finnes det en måte å kreve array-elementer med TypeScript slik at jeg kan ha

type E = keyof T; // Number of properties in T is unknown

la T være definert som i dette eksemplet:

interface T{
   el1:number,
   el2:number,
   el3:number
}

Den resulterende typevakten bør sjekke at alle og bare egenskapene til T skal være eksponert i den resulterte arrayen. For eksempel ved å bruke eksemplet T fra ovenfor:

[{"arg":"el1"},{"arg":"el2"},{"arg":"el3"}]  //only correct option
[{"arg":"el1"},{"arg":"el2"}]  // should fail
[{"arg":"el1"},{"arg":"el2"},{"arg":"el2"},{"arg":"el3"}]  // should fail
[{"arg":"el1"},{"arg":"el2"},{"arg":"el8"}]  // should fail

Akkurat nå bruker jeg

type ITestElements = {
    fn: E
}[];

som bare dekker det andre eksempelet som positivt også.

Svar #1

Jeg skal definere dette:

type Arg<T> = T extends any ? { arg: T } : never;

slik at vi kan bruke Arg<E> (tilsvarende {arg:"el1"}|{arg:"el2"}|{arg:"el3"}) i det som følger.

Det beste du kan håpe på her, ville være en eller annen generisk hjelpefunksjon verifyArray() som vil håndheve begrensningene for argumentet sitt, som er:

  • et array av elementer fra en union
  • mangler ingen elementer fra foreningen
  • og uten duplikater

Og det kommer til å bli stygt.

Det finnes ingen brukbar konkret type som vil håndheve dette for unioner som inneholder mer enn omtrent seks elementer. Det er mulig å bruke noen ulovlig-rekursive eller lovlig-ikke-rekursive-men-tidkrevende typedefinisjoner for å ta en unionstype som 0 | 1 | 2 | 3 og gjøre den om til en union av alle mulige tupler som oppfyller kriteriene dine. Det ville produsere noe som

type AllTuples0123 = UnionToAllPossibleTuples<0 | 1 | 2 | 3>

som ville tilsvare

type AllTuples0123 =
    | [0, 1, 2, 3] | [0, 1, 3, 2] | [0, 2, 1, 3] | [0, 2, 3, 1] | [0, 3, 1, 2] | [0, 3, 2, 1]
    | [1, 0, 2, 3] | [1, 0, 3, 2] | [1, 2, 0, 3] | [1, 2, 3, 0] | [1, 3, 0, 2] | [1, 3, 2, 0]
    | [2, 0, 1, 3] | [2, 0, 3, 1] | [2, 1, 0, 3] | [2, 1, 3, 0] | [2, 3, 0, 1] | [2, 3, 1, 0]
    | [3, 0, 1, 2] | [3, 0, 2, 1] | [3, 1, 0, 2] | [3, 1, 2, 0] | [3, 2, 0, 1] | [3, 2, 1, 0]

Men for en inndataunion av n elementer som vil produsere en utdataunion av n! (det er n-fakultet) utdata, som vokser veldig raskt med n. For ditt eksempel "el1"|"el2"|"el3" ville det være greit:

type AllPossibleTuplesOfArgs = UnionToAllPossibleTuples<Arg<E>>;
const okay: AllPossibleTuplesOfArgs = 
  [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el3" }]; // okay
const bad1: AllPossibleTuplesOfArgs = [{ "arg": "el1" }, { "arg": "el2" }]; // error!
const bad2: AllPossibleTuplesOfArgs = // error!
  [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }];
const bad3: AllPossibleTuplesOfArgs = 
  [{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]  // error!

men jeg antar at du vil ha noe som ikke krasjer kompilatoren din når objektet ditt har syv eller flere egenskaper. Så la oss gi opp UnionToAllPossibleTuples og enhver konkret type.

Så hvordan ville verifyArray() se ut?

Først la oss lage en typefunksjon kalt NoRepeats<T> som tar en tuple-type T og returnerer det samme som T hvis og bare hvis T ikke har noen gjentatte elementer... ellers returnerer den en modifisert tuple som T ikke kan tildeles. Dette vil tillate oss å lage begrensningen T extends NoRepeats<T> for å si "tuple-typen T har ingen gjentatte elementer". Her er en måte å gjøre det på:

type NoRepeats<T extends readonly any[]> = { [M in keyof T]: { [N in keyof T]:
    N extends M ? never : T[M] extends T[N] ? unknown : never
}[number] extends never ? T[M] : never }

Så NoRepeats<[0,1,2]> er [0,1,2], men NoRepeats<[0,1,1]> er [0,never,never]. Deretter kan verifyArray() skrives som dette:

const verifyArray = <T>() => <U extends NoRepeats<U> & readonly T[]>(
    u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
) => u;

Det krever en type T å sjekke mot, og returnerer en ny funksjon som sørger for at argumentet ikke har duplikater (fra U extends NoRepeats<U>), kan tilordnes til T[] (fra & readonly T), og mangler ingen elementer av T (fra & ([T] extends [U[number]] ? unknown : never)). Ja, det er stygt. La oss se om det fungerer:

const verifyArgEArray = verifyArray<Arg<E>>()
const okayGeneric = verifyArgEArray([{ "arg": "el3" }, { "arg": "el1" }, { "arg": "el2" }]); // okay
const bad1Generic = verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }]); // error
const bad2Generic = // error
    verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }]);
const bad3Generic = // error
    verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]);

Så det fungerer.

Begge disse tvinger deg til å kjempe med typesystemet. Du kan muligens lage en byggekklasse som i dette svaret som fungerer bedre med typesystemet, men som medfører enda mer kjøretidsoverhead, og som kan hevdes å bare være litt mindre stygg.

Ærlig talt ville jeg foreslått å prøve å refaktorere koden din slik at du ikke trenger TypeScript for å håndheve dette. Det enkleste er å kreve at et objekt har disse verdiene som nøkler (f.eks. bare lage en verdi av typen T eller muligens Record<keyof T, any>) og bruke det i stedet for (eller før du produserer) et array. Oh well, håper dette hjelper. Lykke til!

Lenke til kode på lekeplassen

Svar #2

Jeg har en løsning for å håndheve ved kompilering at et array inneholder alle verdier av en vilkårlig type T. Det er ganske nært det du ønsker - det mangler sjekken for å hindre duplikater - men noen kan ha behov for å ikke hindre duplikater (for eksempel har et annet spørsmål merket som en duplikat av dette ikke det kravet).

Type NoRepeats fra @jcalz sitt svar kan sannsynligvis kombineres med denne hvis du vil håndheve det ekstra kravet.

Vi kan håndheve at et array inneholder hver verdi av typen T ved å skrive en generisk funksjon med to typeparametere. T er en vilkårlig type, og A er typen til arrayet. Vi ønsker å sjekke at T[] og A er identiske. En fin måte å gjøre dette på er å skrive en betinget type som løses til never hvis disse to typene ikke er subtyper av hverandre:

type Equal<S, T> = [S, T] extends [T, S] ? S : never;

Vi vil spesifisere typeparameteren T eksplisitt, for å fortelle kompilatoren hva som skal sjekkes, men vi vil at A skal bli utledet fra den faktiske verdien, slik at sjekken blir meningsfull. Typescript tillater ikke å spesifisere noen, men ikke alle typeparametere for en funksjon, men vi kan omgå dette med currying:

function allValuesCheck<T>(): <A extends T[]>(arr: Equal<A, T[]>) => T[] {
    return arr => arr;
}

Da kan denne funksjonen brukes til å be kompilatoren sjekke at en array-literal inneholder alle mulige verdier av en vilkårlig type:

type Foo = 'a' | 'b';

// ok
const allFoo = allValuesCheck<Foo>()(['a', 'b']);
// type error
const notAllFoo = allValuesCheck<Foo>()(['a']);
// type error
const someNotFoo = allValuesCheck<Foo>()(['a', 'b', 'c']);

Ulempen er at typefeilene ikke er informative; den første feilmeldingen sier bare Type 'string' kan ikke tildeles type 'never', og den andre feilmeldingen sier bare Type 'string' kan ikke tildeles type 'Foo'. Så selv om kompilatoren informerer deg om feilen, forteller den deg ikke hvilken som mangler eller er feil.

Lekeromlenke

Relaterte artikler