generics - Är det möjligt att definiera en typ som representerar en funktion med en parameter utan 'any' i TypeScript?


Uppenbarligen är detta ganska trivialt med hjälp av nyckelordet any:

type SingleParamFunction = (arg: any) => any;

const foo: SingleParamFunction = (x: number): number => x + 1;

Jag har försökt följande:

type SingleParamFunction = (arg: unknown) => unknown;

const foo: SingleParamFunction = (x: number): number => x + 1;

Men kompilatorn klagar på att 'unknown' inte kan tilldelas typen 'number'.

Jag är nyfiken på om det är möjligt att göra det utan att använda any, kanske en lösning som använder generics.


Ett potentiellt användningsfall kan se ut ungefär som följande:

interface Foo<I, O> {
    input: I;
    transform: (x: I) => O;
}
const foos: Foo<any, any>[] = [
    {
        input: 1,
        transform: (x: number) => x + 1,
    },
    {
        input: 'hello',
        transform: (s: string) => s + ' world',
    },
];
for (const foo of foos) {
    console.log(foo.transform(foo.input));
}

Svar #1

Det finns flera lösningar på ditt problem som är lika giltiga men successivt förbättrar typ-säkerheten.

Det första är det triviala fallet du presenterade: (x: any) => any. Trots tumregeln "använd aldrig any" kan en dömande användning av any vara ett giltigt val. Om allt du behöver är att tala om för kompilatorn att funktionen accepterar ett enda argument av vilken typ som helst, varför inte?

Den andra är mer sund och föreslogs redan av fjc i kommentaren:

Varför inte använda en generisk som denna: type SingleParamFunction=(arg: T) => T; const foo: SingleParamFunction=(x) => x + 1;?

Detta löser problemet med kontravarians, men kompilatorn kommer att klaga på signaturerna för transform-metoderna eftersom unionen kommer att passeras igenom, så x och returtypen kommer att härledas som en union (till exempel string | number):

interface Foo<I> {
    input: I;
    transform: SingleParamFunction<I>;
}
const foos: Foo<string | number>[] = [
    {
        input: 1,
        transform: (x) => x + 1, //Type 'string' is not assignable to type 'number'.(2322)
    },
    {
        input: 'hello',
        transform: (s: string) => s + ' world', //Type 'string | number' is not assignable to type 'string'.
    },
];
for (const foo of foos) {
    console.log(foo.transform(foo.input));
}

Detta är dock inte lösningens problem. Eftersom foos är typad som en muterbar array, vet inte kompilatorn vilken typ input eller transform's x och returtyp i en viss medlem har, bara att det borde vara string | number.

Om du tar bort typannoteringen kommer du dock att se att typen på arrayen infereras korrekt. Detta borde ge dig en ledtråd om vad du behöver kontrollera och leder oss till en tredje lösning:

({
    input: number;
    transform: (x: number) => number;
} | {
    input: string;
    transform: (s: string) => string;
})[]

Så, hur skulle du göra det? Mappade typer till undsättning! Du behöver definiera en "array av Foo's där för varje typ i möjliga input-typer finns det möjligtvis ett medlemsobjekt som är en Foo av denna typ". Här är hur en sådan typ skulle kunna se ut (med hjälp av nyckelomkartläggning):

type validator<I> = Array<{ [T in I as number]: Foo<T> }[number]>;

Och voilá:

//all ok, valid assignment
const bars: validator<string | number> = [
    {
        input: 1,
        transform: (x: number) => x + 1
    },
    {
        input: 'hello',
        transform: (s: string) => s + ' world'
    },
];

const invalid : validator<string|number> = [
    {
        input: "nope", //Type 'string' is not assignable to type 'number'.(2322)
        transform: (x: number) => x + 1
    },
    {
        input: 'hello',
        transform: (s: string) => Infinity //Type 'number' is not assignable to type 'string'.(2322)
    }, 
];

Lekplats