4

structuredClone or lodash.cloneDeep cannot clone functions.

Is there a way to exclude Function type from generic?

I tried object: Exclude<T, {[key: string|number|symbol]: any, apply: any, call: any}> and object: Exclude<T, Function>, both will return a type error when the object is passed in as this of a class.

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 Playground

Is there any way to fix this?

3
  • 1
    Typescript does not currently support negated types.. Commented Oct 3, 2022 at 19:53
  • Does this approach meet your needs? TS doesn't have negated types and generic conditional types are deferred so such things would only work with non-generic arguments, but you could approximate "not a function" as a union of every primitive plus objects-with-no-call plus objects-with-no-apply, which gets most things you'd care about. If that works for your use cases I can write up an answer; if not, what am I missing? (Mention @jcalz in your reply to notify me, if you reply) Commented Oct 3, 2022 at 20:02
  • Okay I’ll write up an answer when I get a chance Commented Oct 3, 2022 at 20:31

2 Answers 2

9

If TypeScript had negated types of the sort implemented in (but never merged from) microsoft/TypeScript#29317 then you could just write

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

and be done with it. But there is no not in TypeScript, at least as of TypeScript 4.8, so you can't.


There are various ways to try to simulate/emulate not. One way is to do what you're doing: write a conditional type that acts like a circular generic constraint, which you've done here:

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

This works well for parameters of specific types, like

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

but when the parameter is itself of a generic type, then it can break down. And the polymorphic this type of this inside a class method is an implicit generic type parameter. (That is, it is treated like some unknown type constrained to the class instance type). The compiler doesn't know how to verify assignability of this to Exclude<this, Function>, which makes sense because the compiler does not know how to say that some subtype of Test might not also implement Function.

You can work around it by widening this to a specific supertype, like Test:

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

Another approach to approximating negated types is to carve up the set of all possible types into pieces that mostly cover the complement of the thing you're trying to negate. We know that no primitive types are functions, so we can start with

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

And then we can start adding in object types that are also not functions. Probably any arraylike type will not also be a function:

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

And any object that doesn't have a defined apply property is also not a function:

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

And any object that doesn't have a defined call property is also not a function:

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

Should we keep going or stop there? The above definition of NotFunction will misclassify any objects with both an apply property and a call property. Are we worried about those? Are we likely to run into non-function objects with properties named apply and call? If so, we can add more pieces. Maybe we want to add objects without a bind property. Or objects with bind, call, and apply properties but where each of those properties are themselves primitives, like { call: string | number | ... , apply: string | number | ... }... But at some point we should just stop. Approximating 𝜋 by 3.14 is good enough for lots of use cases, and it's usually more trouble than it's worth to approximate it by 3.141592653589793238462643383. Let's just use the above definition.

Anyway, now let's try to use NotFunction in place of 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
    }
}

Those behave as desired. It's still possible that some subtype of Test could be assignable to Function, but the compiler doesn't care and neither do we, I think.

And of course we don't care about this:

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

because if we did we'd have to add some more digits of 𝜋 to our approximation of NotFunction to deal with it.


Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

0

Alternatively, use the following:

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

Usage:

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

Which will cause the following message if a Function type was passed (or not removed from a union):

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

2 Comments

Not to be harsh, but this and its shortcomings are described in the second paragraph of @jcalz's answer. (Your ExcludeFunction<T> is pretty much equivalent to Exclude<T, Function>.)
Not at all - I must have missed that! Always happy to learn :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.