I was playing around with TypeScript and found that applying Omit<T, K>
on a callable function, makes it no longer callable:
declare function myCallableFunction(): void;
myCallableFunction(); // valid
type NonOmittedFunction = typeof myCallableFunction;
declare const myNonOmittedFunction: NonOmittedFunction;
myNonOmittedFunction(); // valid
type OmittedFunction = Omit<typeof myCallableFunction, 'foobar'>;
declare const myOmittedFunction: OmittedFunction;
myOmittedFunction(); // This expression is not callable.
// Type 'OmittedFunction' has no call signatures.
Why is this?
Here is a heavily contrived example as to where you may want to do something like this:
declare type CallCountingFunction = (() => void) & { count: number }
const myFunction: CallCountingFunction = (() => {
const x = () => {};
x.count = 0;
return x;
})()
myFunction.count; // valid
myFunction() // valid
type OmittedFunction = Omit<CallCountingFunction, 'count'>;
declare const myOmittedFunction: OmittedFunction;
myOmittedFunction(); // This expression is not callable.
// Type 'OmittedFunction' has no call signatures.
This appears to be true for all generic utility types that involve remapping such as Partial<T>
and Required<T>
.
CodePudding user response:
The utility types aren't magic, they're actually all written in Typescript and we can view them. Omit
is defined in terms of Pick
and Exclude
.
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Nothing crazy yet, but Pick
is defined as
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
So Pick<T, K>
is always defined to be an object type with indexed keys. There's no call signature here. It's easy to think of Omit
as "take my type T
and remove some specific things", but internally it really is "create a new object type that has T
's keys, minus some things", and since the ability to call a function is not an object key, it doesn't get transferred. The analogy breaks down since functions are, fundamentally, not really designed to be used this way.