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

Extension methods as part of Language Service (no compile time or runtime) #35280

Closed
5 tasks done
staeke opened this issue Nov 22, 2019 · 7 comments
Closed
5 tasks done
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@staeke
Copy link

staeke commented Nov 22, 2019

Search Terms

extension methods, language service, compiler api

Suggestion

With the risk of you stopping to read here, this is related to extension methods: #9

I think @RyanCavanaugh made some great points about all the pitfalls of rewriting callsites, and modifying prototypes isn't great either. But here's my thinking. There's essentially 2 things extension methods bring:

  1. discovery
  2. code beauty

I value 1. much higher. Which leads me to my suggestion. Why not just bring up an extension to the language service for autocomplete? And an appropriate transformation at code write time.

Use Cases

  1. Discovery of code relating to a type, and to be able to "stay in the flow"
  2. All the use cases why people like extension methods

Examples

Consider e.g. the following code:

@extension
function isLeapYear(d:Date):boolean { ... }

Then whenever somebody types getSomeDate(). (notice the trailing .), the autocomplete list would show Date members, but also any found extension methods (maybe with a slightly altered visual representation). Say I toggled down and chose isLeapYear, then the code would be transformed to isLeapYear(getSomeDate()) with the marker in the end. Or if multiple arguments are expected maybe after the first one.

For supporting the optional chaining operator after typing getSomeDate().?, my suggestion would be transform to something like applyOpt(getSomeDate(), isLeapYear)

If you wanted to be fancy, you could rather transform it to a global pipe function (see tc39/proposal-pipeline-operator#146). Something like:

pipe(
    getSomeDate(),
    isLeapYear
)

The optional chaining would maybe be something like

pipeOptional(
    getSomeDate(),
    isLeapYear
)
//or...
pipe(
    getSomeDate(),
    optional(isLeapYear)
)

Interfaces would then be naturally supported as well

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.

#9

@staeke
Copy link
Author

staeke commented Nov 22, 2019

I also want to be clear - I don't know to what extent Language Services deliver autocomplete functionality. If not, I guess you could argue that "it's up to IDE developers". But this won't happen unless there's a general backing. So in that case, I'd urge Typescript to at least say something like "For Visual Studio we'll support this according to these principles". That would probably pave way for others to follow

@RyanCavanaugh RyanCavanaugh added 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 labels Nov 22, 2019
@RyanCavanaugh
Copy link
Member

I feel like this would just lead to endless fights on DefinitelyTyped about whether or not it was appropriate to mark any given function as extension

@RyanCavanaugh RyanCavanaugh added Too Complex An issue which adding support for may be too complex for the value it adds and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Nov 22, 2019
@RyanCavanaugh
Copy link
Member

Thinking about it a bit more, this would just be extremely problematic to implement in a way that appeared to work consistently - TS would literally have to query every extension method to see if its parameter type was compatible with the operand. This isn't something that fits in the perf budget for completions. And something we've learned from auto-import is that "but it's nice when it does work" language service features aren't good enough; it works 100% of the time or people think it's broken and bad.

You can always write a language service plugin if you want to add this to your own experience.

@staeke
Copy link
Author

staeke commented Nov 22, 2019

I feel like this would just lead to endless fights on DefinitelyTyped about whether or not it was appropriate to mark any given function as extension

Fair point, maybe for a feature like this to work well is that DefinitelyTyped never adds that feature - it's only per team to "define their own", but with the ability to say something like "treat lodash's foo as extension"

TS would literally have to query every extension method to see if its parameter type was compatible with the operand. This isn't something that fits in the perf budget for completions.

I guess this comes down to normal case vs worst case, and I understand you must cater for the worst case too. But, isn't this something that hits C# or others too?

You can always write a language service plugin if you want to add this to your own experience.

Thanks, maybe more appropriate

@hax
Copy link

hax commented Nov 30, 2019

notice the trailing .

@staeke I really agree with you that we need extension methods. But we'd better not overload . because as I understand, the design goal of TS is keep everything same as JavaScript except addition of type system, and it's very unlikely JavaScript engines would agree any change to . semantics.

I would suggest we use a separate notation for extensions, for example, ::. So you can write date::isLeapYear(). Actually it have already been proposed from 2015!

Unfortunately the bind operator is staled and never advanced to stage 1 in TC39 process (though babel plugin is available many years). But I think we still have chance to revive it. Actually I have been working on reforming the old proposal about two months, and very coincidental, I plan to rename it to Extensions Proposal 😜 and want to present it in TC39 meetings next year.

TS would literally have to query every extension method to see if its parameter type was compatible with the operand.

@RyanCavanaugh Not every , but only extension methods imported or created in current scope. Actually if we want provide autocomplete suggestions for pipeline operator, we will have even worse situation. If we really worry about performance, we always have a compromise solution: do not provide autocomplete for extensions and pipeline by default but allow users enable it if they want.

I feel like this would just lead to endless fights on DefinitelyTyped about whether or not it was appropriate to mark any given function as extension

with the ability to say something like "treat lodash's foo as extension"

This is must-to-have! We need some easy way to use current libraries as extensions. For example:

import * as lo from 'lodash'
const extension {first, last} = lo // just a example, we still need to investigate the syntax
let a = [1, 2, 3]
a::last() // 3

@staeke
Copy link
Author

staeke commented Mar 28, 2020

@RyanCavanaugh and @hax: Extension methods already exists! Eh...what? Well, I thought I'd write a language service plugin, only to realize, they actually work well enough for me already, with symbols and classic Javascript prototype extensions. The idea is to just extend e.g. Date with a isLeapYear (or Object with IFooExtensionMethod) and add the right typings. See https://github.com/staeke/ts-extension-methods. This requires typescript@next since some bug for auto-complete suggestions was recently fixed. Please tell me if there's something I've missed!

In terms of syntax, this works and is terse on the consumer side, but maybe a little verbose on the provider side (less of a problem). If, and that's a big if as it's not necessary, you wanted to fix that here are some thoughts:

// First simplify extending types in general, by skipping the declare module syntax.
// Rather reuse "extends" (symbol after is non-ambiguous, auto-import works.)
// Also you can choose whether it's just typings (abstract, no implementation) 
// or an expected prototype override (implementation) which will add to prototype
// On interfaces, this means Object.prototype
extends Date {
  isLeapYear(this: Date) { .. }
}

// Now we want symbols, and a way to export those so we allow that inline
extends Date {
  [export symbol isLeapYear]: function(this: Date) {...}
}

This would be equivalent to

export const isLeapYear = Symbol()

function isLeapYearImpl(this: Date) { .. }

declare global {
    export interface Date {
        [isLeapYear]: typeof isLeapYearImpl;
    }
}

Date.prototype[isLeapYear] = isLeapYearImpl;

@spyro2000
Copy link

spyro2000 commented Jul 7, 2023

I honestly don't care about the technical insights anymore. I just want to get extension methods work in some way in typescript. It's just a pain when you came from C# or Kotlin.

getSomething(url)
 .transformIt(it => it+1)
 .mapToSometing(myParameter)

is so much more easy to read than

mapToSomething(transform(getSomething(url), it +1), myParameter)

Due to treeshaking, we saw many fluent APIs killed and replaced by dinstinct functions in the last years. And you can't see any suggestions of the IDE anymore, you just have to know what you can use. It really is a major drawback.

@microsoft microsoft locked as resolved and limited conversation to collaborators Jul 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests

4 participants