structuredClone
or lodash.cloneDeep
can not 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)
Is there any way to fix this?
Thanks
CodePudding user response:
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