TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Allow to add a call signature to the Mapped Type OR to remove all `Function.prototype` methods

Open anurbol opened this issue 7 years ago • 12 comments

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)

anurbol avatar Oct 05 '18 18:10 anurbol

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

RyanCavanaugh avatar Oct 08 '18 11:10 RyanCavanaugh

You could also use never in place of null

RyanCavanaugh avatar Oct 08 '18 11:10 RyanCavanaugh

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

trickeyd avatar Jan 13 '20 08:01 trickeyd

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.

fwh1990 avatar Jul 10 '20 00:07 fwh1990

+1

Trying to create a concise function builder, and having the interesting methods polluted by the function prototype.

nth-commit avatar Sep 15 '20 01:09 nth-commit

+1 would love to exclude extra these function properties to make the IDE experience better

zachkirsch avatar Oct 24 '22 00:10 zachkirsch

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.

atablash avatar Oct 26 '22 01:10 atablash

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

Krombik avatar May 20 '23 20:05 Krombik

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.

geoffreytools avatar Sep 03 '23 13:09 geoffreytools

@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

rotu avatar Dec 25 '23 20:12 rotu

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

jcalz avatar Jul 31 '24 19:07 jcalz

+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.

ahzvenol avatar Mar 10 '25 09:03 ahzvenol

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 :)

Image

guyutongxue avatar Oct 23 '25 10:10 guyutongxue