Typescript generisk typ för allt utom en funktion


structuredClone eller lodash.cloneDeep kan inte klona funktioner.

Finns det ett sätt att exkludera Function-typen från generisk?

Jag försökte object: Exclude<T, {[key: string|number|symbol]: any, apply: any, call: any}> och object: Exclude<T, Function>, båda kommer att ge ett type-fel när objektet skickas in som this i en klass.

function cloneAnythingButFunction1<T>(object: Exclude<T, {[key: string|number|symbol]: any, apply: any, call: any}>): T{
    return structuredClone(object)}

function cloneAnythingButFunction2<T>(object: Exclude<T, Function>): T{
    return structuredClone(object)
}

// Expect error:
cloneAnythingButFunction1(cloneAnythingButFunction1)
cloneAnythingButFunction2(cloneAnythingButFunction2)

// Expect no error:
class Test{
    clone1(){
        return cloneAnythingButFunction1(this) // error
    }
    clone2(){
        return cloneAnythingButFunction2(this) // error
    }
}
const test = new Test()
cloneAnythingButFunction1(test)
cloneAnythingButFunction2(test)

TypeScript-lekplats

Finns det något sätt att fixa detta?

Svar #1

Om TypeScript hade negativa typer av den typ som implementerats i (men aldrig sammanslagits från) microsoft/TypeScript#29317 då kunde du bara skriva

// Don't do this, it isn't valid TypeScript:
declare function cloneAnythingButFunction<T extends not Function>(object: T): T;

och bli klar med det. Men det finns ingen not i TypeScript, åtminstone inte i TypeScript 4.8, så du kan inte.


Det finns olika sätt att försöka simulera/emulera not. Ett sätt är att göra det du gör: skriva en villkorlig typ som fungerar som en cirkulär generisk begränsning, vilket du har gjort här:

declare function cloneAnythingButFunction<T>(object: Exclude<T, Function>): T;

Detta fungerar bra för parametrar av specifika typer, som

cloneAnythingButFunction(123); // okay
cloneAnythingButFunction({ a: 1, b: 2 }); // okay
cloneAnythingButFunction(Test) // error
cloneAnythingButFunction(() => 3) // error
cloneAnythingButFunction(new Test()); // okay    

men när parametern själv är av en generisk typ, då kan det gå fel. Och den polymorfa this-typen av this inuti en klassmetod är en implicit generisk typparameter. (Det vill säga, den behandlas som någon okänd typ begränsad till klassinstanstypen). Kompilatorn vet inte hur den ska verifiera tilldelningsbarheten av this till Exclude<this, Function>, vilket är rimligt eftersom kompilatorn inte vet hur den ska säga att någon subtyp av Test kanske inte också implementerar Function.

Du kan kringgå det genom att vidga this till en specifik övertyp, som Test:

class Test {
    clone1() {
        const thiz: Test = this;
        return cloneAnythingButFunction(thiz); // okay
        // return type is Test, not this
    }
}

Ett annat tillvägagångssätt för att approximera negaterade typer är att dela upp mängden av alla möjliga typer i delar som för det mesta täcker komplementet av det du försöker negatera. Vi vet att inga primitiva typer är funktioner, så vi kan börja med

type NotFunction = string | number | boolean | null | undefined | bigint | ....

Och sedan kan vi börja lägga till objekttyper som inte heller är funktioner. Förmodligen kommer vilken arraylik typ som helst inte heller att vara en funktion:

type NotFunction = string | number | boolean | null | undefined | bigint | 
   readonly any[] | ...

Och alla objekt som inte har en definierad apply-egenskap är heller inte en funktion:

type NotFunction = string | number | boolean | null | undefined | bigint | 
   readonly any[] | { apply?: never, [k: string]: any } | ...

Och alla objekt som inte har en definierad call-egenskap är heller inte en funktion:

type NotFunction = string | number | boolean | null | undefined | bigint | 
   readonly any[] | { apply?: never, [k: string]: any } | 
   { call?: never, [k: string]: any };

Ska vi fortsätta eller stanna där? Ovanstående definition av NotFunction kommer att felklassificera alla objekt som har både en apply-egenskap och en call-egenskap. Är vi oroade över sådana? Är det troligt att vi stöter på icke-funktionella objekt med egenskaper som heter apply och call? Om så är fallet kan vi lägga till fler delar. Kanske vill vi lägga till objekt utan en bind-egenskap. Eller objekt med bind, call och apply-egenskaper men där var och en av dessa egenskaper själva är primitiva, som { call: string | number | ... , apply: string | number | ... }... Men vid någon punkt bör vi bara sluta. Att approximera 𝜋 med 3,14 är tillräckligt för många användningsfall, och det är vanligtvis mer besvär än det är värt att approximera det med 3,141592653589793238462643383. Låt oss bara använda ovanstående definition.

Hur som helst, låt oss nu försöka använda NotFunction istället för not Function:

declare function cloneAnythingButFunction<T extends NotFunction>(object: T): T;

cloneAnythingButFunction(123); // okay
cloneAnythingButFunction({ a: 1, b: 2 });
cloneAnythingButFunction(Test) // error
cloneAnythingButFunction(() => 3) // error
cloneAnythingButFunction(new Test()); // okay    

class Test {
    clone1() {
        return cloneAnythingButFunction(this); // okay
    }
}

De beter sig som önskat. Det är fortfarande möjligt att någon subtyp av Test skulle kunna tilldelas till Function, men kompilatorn bryr sig inte och det gör inte vi heller, tror jag.

Och naturligtvis bryr vi oss inte om detta:

cloneAnythingButFunction({apply: "today", call: "1-800-TYP-SCRP"}); // error oh noez

för om vi gjorde det skulle vi behöva lägga till några fler siffror av 𝜋 till vår approximation av NotFunction för att hantera det.


Lekplatslänk till kod

Svar #2

Som ett alternativ, använd följande:

type ExcludeFunction<T> = T extends Function ? never : T

Användning :

function cloneAnythingButFunction<T>(value: ExcludeFunction<T>) { ... }

Vilket kommer att orsaka följande meddelande om en Function-typ skickades (eller inte togs bort från en union):

TS2345: Argument of type  T  is not assignable to parameter of type  ExcludeFunction<T>

Relaterade artiklar