TypeScript
TypeScript copied to clipboard
Allow to add a call signature to the Mapped Type OR to remove all `Function.prototype` methods
Search Terms
add a call signature to the Mapped Type; remove all Function.prototype methods
Suggestion
Being able to create a callable type, but without Function.prototype methods.
Use Cases
Imagine one creates some API that exposes a function that is (because function is an object) also have some custom properties. This function is not supposed to be call-ed, apply-ed, bind-ed, etc, in fact all the Function.prototype methods are removed from it (e.g. with setPrototypeOf..null). However IDE will still suggest the Function.prototype methods.
Desired behavior:
let callableObject = () => 'foo'
Object.setPrototypeOf(callableObject, null)
callableObject.bar = 'baz'
callableObject() // 'foo'
callableObject // {bar: 'baz'}
callableObject.call // <---------- error
Current behavior:
let callableObject = () => 'foo'
Object.setPrototypeOf(callableObject, null)
callableObject.bar = 'baz'
callableObject() // 'foo'
callableObject // {bar: 'baz'}
callableObject.call // <---------- OK
What I tried:
type ExcludeFunctionPrototypeMethods<T extends () => any> = {
[K in Exclude<keyof T, keyof Function>]: T[K]
}
// the type above "lacks a call signature" and I have no Idea how to add it there.
Checklist
My suggestion meets these guidelines:
- [x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. new expression-level syntax)
type Unfunction<T> = T & { call: null; apply: null; bind: null; }
function getUnfunction<T extends Function>(callableObject: T): Unfunction<T> {
Object.setPrototypeOf(callableObject, null);
return callableObject as Unfunction<T>;
}
let callableObject = getUnfunction(() => 'foo');
callableObject() // 'foo'
callableObject // {bar: 'baz'}
callableObject.call() // <---------- error
You could also use never in place of null
The problem with this method is that all the prototype props still exist on the type, just as 'never'. I feel there should be a way to explicitly 'mask' types out completely so that they don't appear in the type at all - for the sake of a clean api.
Been talking about it here: https://stackoverflow.com/questions/59700434/typescript-omitting-a-function-prototype-from-a-type-extending-any?noredirect=1#comment105570732_59700434
this is kinda linked too: https://github.com/microsoft/TypeScript/issues/29261
I agree with @anurbol . Sometimes we have custom property on function like fn.foo = 123, and we want user see this property from IDE tips, however, so many unexpected properties like apply | call | bind | arguments ... are shown, in other words fn.foo is hidden from them, user is very hard to find custom things in a second.
@RyanCavanaugh Maybe this issue should not be closed.
+1
Trying to create a concise function builder, and having the interesting methods polluted by the function prototype.
+1 would love to exclude extra these function properties to make the IDE experience better
We could really use a feature where mapped types would not strip call and construct signatures or there would be a new mapping type that would perform mapping on call and/or construct signatures instead of (or in addition to) regular property mapping.
For callables, this would be very useful for implementing any sort of callable interfaces.
For newables, perhaps more common, you could perform property mappings on constructors with additional properties (classes with static fields) - without loosing the construct signature.
I found a workaround:
declare class SomeName {
static someField: any;
}
type Identity<T> = T;
interface SomeName extends Identity<typeof SomeName> {
(...args: any[]): any;
}
const someName: SomeName
this solution allows someName to be called and the someField property to be first in the suggestion list
I like @Krombik's workaround and I think this should be the default display order for properties on callable objects.
A big limitation though is that someField needs to be static, so it cannot reference any type parameter on SomeName.
I feel that it is generally a terrible idea to remove properties and methods from the function prototype just to get cleaner tooltips, unless the implementer truly has a good reason to prescribe how users should call their functions.
@RyanCavanaugh this breaks in TS 4.3.5, where it deduces the type of callableObject as never (I think it's actually broken in 3.9.7, but there is no visible error on calling never). playground link
Is there an existing issue for this?
type Unfunction<T> = T & { call: null; apply: null; bind: null; } function getUnfunction<T extends Function>(callableObject: T): Unfunction<T> { Object.setPrototypeOf(callableObject, null); return callableObject as Unfunction<T>; } let callableObject = getUnfunction(() => 'foo'); callableObject() // 'foo' callableObject // {bar: 'baz'} callableObject.call() // <---------- error
We could really use a feature where mapped types would not strip call and construct signatures or there would be a new mapping type that would perform mapping on call and/or construct signatures instead of (or in addition to) regular property mapping.
For callables, this would be very useful for implementing any sort of callable interfaces.
For newables, perhaps more common, you could perform property mappings on constructors with additional properties (classes with static fields) - without loosing the construct signature.
#29261 seems to be the place for that
+1, I use Proxy to map function properties to another object to create my reactive store. In this case, Function.prototype doesn't exist and is of no use.
Run into this issue, but here is a small tips: mark all keys of Function to @deprecated:
type NotFunctionPrototype = {
/** @deprecated This object do not have function prototype */
apply: never;
/** @deprecated This object do not have function prototype */
bind: never;
/** @deprecated This object do not have function prototype */
call: never;
/** @deprecated This object do not have function prototype */
arguments: never;
/** @deprecated This object do not have function prototype */
caller: never;
/** @deprecated This object do not have function prototype */
prototype: never;
/** @deprecated This object do not have function prototype */
toString: never;
/** @deprecated This object do not have function prototype */
length: never;
/** @deprecated This object do not have function prototype */
name: never;
};
type Foo = ((...args: any[]) => number) & NotFunctionPrototype
declare let foo: Foo;
foo.
//. ^~ sorts all Function.prototype methods at end of suggestion list
And when user typing . after the object, they won't be facing annoying Function.prototype at their IDE suggestion list :)