typescript: Lag en [key,value type]-tuple fra et grensesnitt
Jeg prøver å lage en TypeScript-tupletype (et par), som kun kan inneholde verdier hvor den første verdien er en keyof for et grensesnitt som har gyldige operasjoner som nøkler, og typen til det forventede argumentet som verdi
Jeg antar at koden nedenfor vil forklare bedre hva jeg prøver å oppnå:
interface IEventsAndArguments {
'+': number,
'-': number,
'change_name': string,
}
// Now I want to produce tuples,
// where the first value is in keyof IEventsAndArguments
// and the second value is of the corresponding type
// (but a type with 2 properties name and value would do as well)
// it works for function arguments
function myFunction<K extends keyof IEventsAndArguments>(x:K, y:IEventsAndArguments[K]) {}
myFunction('+', 'a string'); // invalid as expected, '+' needs a number
myFunction('+', 4);
myFunction('change_name', 'John');
function myFunction2<K extends keyof IEventsAndArguments>({x, y}:{x:K, y:IEventsAndArguments[K]}) {}
myFunction2({x: '+', y: 'another string'}); // invalid as expected
myFunction2({x: '-', y: 3});
// it doesn't work when I try to define a tuple type
type TTuple<K extends keyof IEventsAndArguments> = [K, IEventsAndArguments[K]];
// I would like the next tuple to be INVALID
// but it is valid since the second element is of type number | string
const tuple1:TTuple<keyof IEventsAndArguments> = [ '+', 'hello' ]
// What I ultimately would want is something similar to this
type TTuple2<K extends keyof IEventsAndArguments> = [
K,
(v:IEventsAndArguments[K]) => void
];
// I would like the next tuple to be VALID but it is invalid
// since the function argument must be of type number | string
const tuple2:TTuple2<keyof IEventsAndArguments> = [
'+',
(t:number) => {},
];
Her er en lekeplasslenke med den samme koden.
Finnes det en måte å få dette til å fungere på? Kan noen forklare hvorfor det oppfører seg som jeg ønsker med funksjoner, men ikke når jeg definerer en ny type?
Takk.
REDIGER: Jeg tror jeg har funnet en mekanisme som er 'god nok': siden det fungerer for funksjon, kan vi bruke en funksjon til å produsere en gyldig tuple!
// if we use a function to produce the tuple, we can produce 'valid'
// tuples, even if the tuple type itself is not as strict as we'd like...
function tuple<K extends keyof IEventsAndArguments>(
a:K, f:(v:IEventsAndArguments[K]) => void
):TTuple2<K>
{
return [a, f];
}
const tuple3 = tuple('+', (v:number) => v + 1); // valid, as expected
const tuple4 = tuple('+', (v:string) => v + 1); // invalid, also as expected
Men frustrasjonen består: hvorfor fungerer det for funksjoner (begge parametrene 'lenket' til samme oppføring i grensesnittet) men ikke når man oppretter en ny type? Det hadde vært fint om vi også kunne gjøre typen så streng som vi vil på en eller annen måte, fordi akkurat nå kan man fortsatt produsere 'ugyldige' tupler ved å ikke bruke funksjonen.
Åh: også viktig: jeg vil at dette skal være generisk, så uansett hvilke oppføringer folk legger til i grensesnittet senere, skal det fortsatt fungere, så det bør antas at definisjonen av IEventsAndArguments er ukjent og kan utvides andre steder.
Svar #1
Du vil at din TTuple-type enten skal være et tuple av formen ["+", (v: number) => void], eller av formen ["-", (v: number) => void], eller av formen ["change_name", (v: string) => void]. Du kan oppnå dette uten generics bare ved å gjøre TTuple til en union av disse typene:
type TTuple =
["+", (v: number) => void] |
["-", (v: number) => void] |
["change_name", (v: string) => void];
Du kan bekrefte at dette fungerer:
let tuple: TTuple;
tuple = ['+', (t: number) => console.log(t.toFixed())]; // okay
tuple = ['change_name', (t: number) => console.log(t.toFixed())]; // error
tuple = ['change_name', (t: string) => console.log(t.toUpperCase())]; // okay
Selvfølgelig ønsker du ikke å definere TTuple manuelt; det ville være å foretrekke at kompilatoren beregner TTuple som en funksjon av IEventsAndArguments.
En måte å gjøre dette på er å lage en såkalt "distributive object type" (terminologien kommer fra microsoft/TypeScript#47109), hvor du først mapper over IEventsAndArguments-typen for å danne en ny objekttype med de samme nøklene, men hvor egenskapens verdityper er de tilsvarende tuplet-typene, og deretter umiddelbart indekserer inn i denne mappede typen med en union av IEventsAndArguments-nøklene, som produserer en union av tuplene. Slik ser det ut:
type TTuple = { [K in keyof IEventsAndArguments]: [
K,
(v: IEventsAndArguments[K]) => void
] }[keyof IEventsAndArguments];
Når man går gjennom det, itererer den kartlagte typen {[K in keyof]... } over "+", "-" og "change_name"-nøklene for å produsere noe som
{
"+": ["+", (v: IEventsAndArguments["+"] )=> void],
"-": ["-", (v: IEventsAndArguments["-"] )=> void],
"change_name": ["change_name", (v: IEventsAndArguments["change_name"] )=> void]
}
som reduseres til
{
"+": ["+", (v: number) => void],
"-": ["-", (v: number) => void],
"change_name": ["change_name", (v: string) => void]
}
Deretter indekserer vi inn i det med "+" | "-" | "change_name" som produserer unionen ["+", (v: number) => void] | ["-", (v: number) => void] | ["change_name", (v: string) => void] som ønsket.
Nå, hvis du legger til eller endrer egenskaper på IEventsAndArguments-typen, vil TTuple-typen automatisk endres for å tilpasse seg.
Lenke til kode på lekeplassen
