Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow marking functions in interfaces as callback / non-callable #30169

Open
5 tasks done
inad9300 opened this issue Mar 1, 2019 · 8 comments
Open
5 tasks done

Allow marking functions in interfaces as callback / non-callable #30169

inad9300 opened this issue Mar 1, 2019 · 8 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@inad9300
Copy link

inad9300 commented Mar 1, 2019

Search Terms

callable function, non-callable function, callback function type, callback type modifier

Suggestion

A keyword and some language support to identify functions which are not callable (they are only assignable callbacks).

Use Cases

Currently, it is possible to call functions which aren't intended to be called, for example:

const btn = document.createElement('button')
btn.onclick = evt => {} // Correct use
btn.onclick(new MouseEvent('')) // Incorrect use, but no error

It should be possible to declare that onclick is only a callback, and thus you must not call it directly. A keyword such as callback, noncallable or nocall could be used for this purpose, in a spirit similar to the readonly modifier. For example:

interface HTMLButtonElement {
    callback onclick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
}

Similar capabilities to that of readonly should be implemented around it, such as being able to add and remove the modifier through mapped types:

type FullyCallable<T> = {
    -callback [P in keyof T]: T[P];
}

Which would set the stage for later usage in conditional types. For example, this is a way one can currently use to extract writable properties from a type:

type Not<T extends boolean> = T extends true ? false : true

type Equals<X, Y> =
    (<T> () => T extends X ? 1 : 2) extends
        (<T> () => T extends Y ? 1 : 2)
            ? true
            : false

type IsReadonly<O extends Record<any, any>, P extends keyof O> =
    Not<Equals<{[_ in P]: O[P]}, {-readonly [_ in P]: O[P]}>>

type WritableProperties<O extends Record<any, any>> = {
    [P in keyof O]: IsReadonly<O, P> extends true
        ? never
        : P
} extends {[_ in keyof O]: infer U}
    ? U
    : never

type Writable<O extends Record<any, any>> = Pick<O, WritableProperties<O>>

type T = Writable<{
    n: number,
    readonly b: boolean,
    o: {
        readonly s: string // Recursion is possible... But even crazier.
    }
}> // { n: number; o: { readonly s: string; }; }

It would be convenient to be able to do a similar thing with callbacks, identifying them and removing them at will while defining new types.

(A different topic altogether, but I hope declaring types like those above becomes more straightforward in the future, e.g. by having a direct way to identify readonly properties, or by being able to use logical operators as part of conditional types. Feel free to pick up on these ideas and start separate issues... Or I can do it myself if you ask me to. Edit I finally did: #31581, #31579)

Example

The task I have at hand at the moment is to define the following button-spawning function (more generally, I would like to do this for any Element):

type ButtonProperties<T> = any

function button(properties: ButtonProperties<HTMLButtonElement>) {
    const btn = document.createElement('button')
    // Assign all `properties` to `btn`...
    return btn
}

But I would like to define ButtonProperties<T> in such a way that it removed from HTMLButtonElement properties meeting certain criteria, namely:

  • Are readonly. (Managed.)
  • Are index signatures. (Managed.)
  • Are callbacks. (Sort-of managed, with the assumption that all callbacks defined in Elements take as a first argument something extending Event, which is not necessarily fully accurate and it does not scale to user-defined types.)

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh
Copy link
Member

This is just writeonly, right?

@inad9300
Copy link
Author

inad9300 commented Mar 1, 2019

Hmmm... Indeed! I hadn't thought of it this way, but you are right. And I see that there is already an issue for that - thumbs up! #21759

@inad9300 inad9300 closed this as completed Mar 1, 2019
@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Mar 1, 2019
@inad9300
Copy link
Author

inad9300 commented Mar 1, 2019

Watch out for one thing, though! You may want to "read" a callback property, and yet not call it, e.g.

if (!btn.onclick) // read
    btn.onclick = ... // write

What are your thoughts on that?

It is nevertheless very true that my proposal overlaps a lot with writeonly, and that their relationship should be studied closely.

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@inad9300
Copy link
Author

inad9300 commented Mar 4, 2019

Please @RyanCavanaugh, have a look at my last comment.

@RyanCavanaugh
Copy link
Member

I think the issue being described is best solved by writeonly.

There are a lot of functions in the world that are only supposed to be called by certain parts of a program and defining a new (somewhat paradoxical) primitive in the type system to describe that "It just happens that all valid callers of this function are outside the text of my program" is too weird.

If you have your own API that you want to type this way, you can always put an impossible parameter at the end so that you don't accidentally invoke it, e.g.

type GuardedCallback = (n: number, s: string, NOT_FOR_YOU_TO_CALL: "I AM ILLEGALLY CALLING THIS FUNCTION") => void;

@inad9300
Copy link
Author

inad9300 commented Mar 4, 2019

Just a couple of comments:

  • The issue being described isn't correctly solved by writeonly, as shown by the example above, and therefore can't be best solved by writeonly. It would not be possible to incorporate writeonly to native interfaces such as HTMLButtonElement without breaking existing programs using the pattern of the example above. The same would apply to sane third-party libraries.
  • All valid callers of a "callback" function would be inside the library or module defining it, while outside calls would be illegal. Unless of course I misunderstood you, as it is not clear from your comment what the extend of "the text of my program" is.
  • Following from the two previous points, one could not actually be able to write a program using writeonly to somehow mean "callback", at least not without defining two sets of types (one private, one public). That is because the program using writeonly itself will need most of the times to check first if a callback was provided in order not to cause a runtime exception.
  • I don't see any paradox or weirdness, please explain. All I see is a concrete fix for a concrete problem, which empowers users to write safer programs.
  • I wasn't trying to push a particular solution. The guard you propose seems fine at a first glance, although it would minorly impact the resulting JavaScript. But if a solution exists and it ends up being the recommended solution by the TypeScript team, then native interfaces should adopt it, such as those representing DOM elements. In other words, the solution should be good enough so that you are willing to use it yourselves for native interfaces. My feeling is that if the guard proposal is to be followed, a stronger convention would be necessary, as to follow it consistently. One small suggestion that seems more elegant to me is to use never as the type of the extra parameter.
  • I was interested as well in the programmatic identification of such cases from conditional/mapped types. I believe it is very important that TypeScript lets you sanely identify from this context any thing that you are allowed to define in the first place, such as read-only properties, optional properties, index signatures, etc. That said, your guard solution seems to accommodate for this.

@inad9300
Copy link
Author

inad9300 commented Mar 7, 2019

Please @RyanCavanaugh, reopen until completing the discussion.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed Duplicate An existing issue was already created labels Mar 8, 2019
@RyanCavanaugh RyanCavanaugh reopened this Mar 8, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants