Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
Add support for Pipeline Operator #38305
Conversation
This conveniently avoids the binderBinaryExpressionStress test.
forEachChild had incorrect parsing order for pipeline operator expression.
Fixes "before all" hook for "Correct errors for tests/cases/conformance/pipeline/pipeline.ts": Error: Missing child when forEach'ing over node: PipelineExpression-arguments
@typescript-bot pack this |
Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build. |
Nice! I have wondered myself how pipeline operators would play with typescript, especially how the types would flow. You could have went the way of only parsing and type checking, and letting e.g. babel do translation, but since you have also done translation this is really easy to try out. I did test it out and ran in to a type inference limitation which I hope can be fixed or it will be hard to use with complex types: const ns = [1, 2, 3, 4, 5];
function map<T, R>(f: (t: T) => R) {
return (ts: T[]): R[] => ts.map(f);
}
const result = ns |> map(x => x * x);
Here |
@Pokute: If it helps at all I have two old branches where I started to look into implementing pipeline as well:
|
Writing the transformer was actually pretty simple as soon as I had the scanning & parsing in place.
I think that the form that TypeScript seems to internally handle it is In the simplest cases, it feels like it should be possible to resolve the type. Beyond that is out of my expertise. :-) Either way, thank you for an interesting test case! |
Is there a way to try pipeline operator and partial application together? |
When I found this PR I nearly cried. Thank you. |
Hey folks, I stumbled upon this PR from https://github.com/tc39/proposal-pipeline-operator#build-tools. I'm not able to access the tarball mentioned here #38305 (comment) and I'm just wondering if it's been removed intentionally. |
At least I haven't removed the tarball. I wonder if those have limited lifetimes. Any ideas @orta? |
Yep, looks like they timeout - we're having a chat about what to do in the long term (normally this is not a problem for us because they are more active PRs), but for now I'll give it another 10-12 days. @typescript-bot pack this |
Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build. |
Running in the same issue, without this sort of inference the usage of the operator is very limited, not sure if this can help but currently the only place I sow proper inference with a pipe implementation is this (not the operator a simple pipe function): |
This is great, I might take a stab at adding the Hack-style proposal (see tc39/proposal-pipeline-operator#167 (comment) - it's the Smart proposal without the minimal proposal) to Flow. |
One of the greatest updates.
|
YESSSS |
Funny that this is coming back full circle. I've made a comment a while ago regarding the type inference/tooling problem with the pipe, and how to solve it: tc39/proposal-pipeline-operator#143 (comment) |
About that backward type inference problem: In RxJS, TypeScript works just fine for a situation like this: const int$ = of('hello').pipe(map(x => x.length));
// type of int$ is inferred to be Observable<number> This would be equivalent to: const int$ = of('hello') |> map(x => x.length); Why would it be impossible to infer type Map = <T, U>(f: T => U) => Observable<T> => Observable<U> And hence the receiver is the last curried argument. So it feels like it should be inferrable in both cases..? |
FYI: It appears the type inference isn't working that great for more advanced cases like RxJS see this playground example Copied here for convenience: interface Subscription {
unsubscribe(): void;
}
interface Observer<T> {
next(value: T): void;
error(err: any): void;
complete(): void;
}
interface Observable<T> {
subscribe(observer: Partial<Observer<T>>): Subscription;
}
type Op<T, R> = (source: Observable<T>) => Observable<R>;
declare function map<T, R>(fn: (value: T, index: number) => R): Op<T, R>;
declare function filter<T>(fn: (value: T, index: number) => boolean): Op<T, T>;
declare const source: Observable<number>;
const result = source
|> map(x => x + x) // <-- x is `unknown`
|> map(x => x + '!!!') // <-- x is `unknown`
|> filter(x => x.length < 20); // <-- x is `unknown` This is a real shame, because it would be a killer language feature for libraries like RxJS. |
@benlesh @markusjohnsson Seems to be a pattern of type inference needing to be added for its use w/ generics; generic use cases don't seem to be covered yet in |
@typescript-bot pack this |
1 similar comment
@typescript-bot pack this |
Testing features like this out in a project and having to manually update the packed bundle because it expires in about 10 days is very annoying - could we get a feature to tell typescript bot to pack things indefinitely? Thanks! /cc @orta |
Actually, this PR convinces me that the pipeline operator in its current form is not a good match for typescript. However:
For example, this also get not inferred well: interface Observable<T> {}
type Op<T, R> = (source: Observable<T>) => Observable<R>;
declare function map<T, R>(fn: (value: T, index: number) => R): Op<T, R>;
declare function filter<T>(fn: (value: T, index: number) => boolean): Op<T, T>;
declare const source: Observable<number>;
const result = filter(x => x.length < 20)(map(x => x + '!!!')(map(x => x + x)(source))); Libraries like RxJS partially has solved this problem, by having a sick pipe method, that basically allows curried functions to be inferred left to right again. For example, it could be typed like this: export function pipe<X1, X2, X3, R>(
value: X1,
f1: (x: X1) => X2,
f2: (x: X2) => X3,
f3: (x: X3) => R,
): R;
const result = pipe(
source,
map(x => x + x),
map(x => x + '!!!'),
filter(x => x.length < 20),
); But if typescript can not infer curried functions well using the pipeline "natively", TS users will still prefer a custom pipe method above the js operator. It sounds like me, that we may need to reach the same conclusion as rescript did (used to be called reasonml), and move to a pipeline operator that is optimized to work with data first functions: Elixir also uses a pipeline operator that works on data first functions |
@kasperpeulen Your mention of RxJS in your last comment is kind of confusing. The main contributors of RxJS have also campaigned for this version pipeline operator for some time; the version of pipeline operator they've supported that this PR is related to is in fact a drop-in replacement for their implementation. A good example of that can be found here: https://reactive.how/rxjs/explorer Accordingly it can be inferred just fine by TypeScript; it been pointed out by people like @benlesh that this current attempt of realizing that version is not acting as expected well within the capabilities of TypeScript today. Similarly, pipeline operator is also a drop-in replacement for the equivalent in Accordingly, I respectfully disagree we need to prioritize an alternate pipeline operator that "works on data first functions" that isn't as idiomatic or have been as historically leveraged by functional programmers; not opposed to that being pursued as a separate initiative/standard proposed nonetheless. |
@lozandier My point is that the curried function of Rx.js only infer in combination with the pipe method. They don't infer if you would use the curried function directly. For example try this: import { from } from "rxjs";
import { map, filter } from "rxjs/operators";
const result = filter((x) => x.length < 20)(map((x) => x + "!!!")(map((x) => x + x)(from("bla"))));
result.subscribe(console.log);
That is I think the reason why the pipeline operator implemented in this PR, can not infer them. So my point is that I don't think that curried, data last, functions are on itself a good fit for typescript, but that there are other ways to compose functions. The way elixir and rescript tackle function composition using a data-first pipeline is more in line with how functions in javascript/typescript are written today. In that way, you can both call those functions directly, for example: const result = map(from("bla"), (x) => x + x); But also using a pipeline: const result = from("bla") |> map((x) => x + x); In other words, you have multiple ways to call the same kind of function. |
@kasperpeulen I’m not sure what you mean by “change the paradigm of how functions are written in JS” when partial application capabilities & the pipeline operator enable the use of existing functions that JS developers have written just fine. This is particularly true of referential transparent functions that have been augmented by the equivalent of both a variety of FP libraries provide FP devs today while they wait for these two proposals to be supported by the language properly usually through These functions are extremely common, particularly when writing optimally testable JS anyway; I’m not sure what functions would have to be majorly re-written for use w/ the pipeline operator when combined w/ the partial application proposal. The pipeline operator on its own elegantly supports single arg (unary) referential transparent functions while combined w/ the partial application proposal enables it to handle all typical functions JS write today just fine. Without the latter being available to the language for a long time is where I can see your concern having merit forcing point-free programming all the time when using the pipeline operator; that said, all steps point to the pipeline operator & partial application capabilities being combined or ratified simultaneously to alleviate your concern. Currently the F#-style, split mix, & smart mix are proposal variants of the pipeline operator that explicitly do so. The latter 2 are explicitly a florian's compromise of accepting some of the ideas of a recently proposed new style (Hack-style) from a perspective similar to yours being cautious of forcing point-free programming w/ the use of pipeline-operator. Split mix & F#-style I find the most promising variants on the matter. Both are iterative approaches to enabling the minimal proposal to be what it is prior to them augmenting it further with concerns that are beyond the uses approximated today by |
With the partial application proposal also in, you indeed dont have to rewrite existing javascript, but it is still more elegant to use the curried variant of the same function (to avoid the ?). I guess the js ecosystem will move more to curried, data last functions even with the partial application syntax. The question is, do we want that? When data last, curried functions, when called directly as shown above, infer very poorly the generic parameters, which seems to be a core design limitation of typescript. Or are there better solution for function composition in typescript? |
@kasperpeulen While I prefer point-free programming, it's purely subjective that a curried variant of the same function is "still more elegant". As far as the two capabilities "moving the JS ecosystem" in a particular direction, that's up for the JS community to decide; not exactly sure who is "we" in your original comment & why their wants should outweigh the JS community. The existence of these proposals won't force the JS community in a particular direction; these capabilities being prevalent in codebases after being ratified would merely signal & verify people wanted to go this direction and them being available natively finally more easily enabled that. The existence of the pipeline operator & partial application proposal natively unblocks barrier of entry selling & communicating longly-leveraged patterns of writing declarative, functional code involving left-to-right composition, clearer mix-in patterns, & so on in environments where every byte counts compared to importing & writing ad hoc code to approximate their behavior with limitations a native solution doesn't. With these capabilities in the language soon, it will be marketed that there are "new" ways to write declarative, functional code in JS as a result. That's fine; similar to how futures (Promises) in JS was similarly marketed (though that went a little extreme when it was shoehorned in places it's not a good fit for like async HTTP requests, but I digress). As far as limitations w/ TypeScript's type system: TypeScript core maintainers are a brilliant bunch; at the end of the day JS doesn't revolve around TypeScript; as a superset of JS they'll refactor TypeScript as needed to continue to be that in good faith. It can be argued w/ the pipeline operator types flow much easier, not the other way around. |
That is fine, if you believe this is the best way to write declarative functional code in JS using curried data last functions. That is what I doubt. Your argument are not really argumenting why data last curried functions with say a F# pipeline are a better fit for javascript, than say a data first “normal” function with an elixir like pipeline. This article goes in detail about the pros and cons about those approaches: Probably better to move this discussion to the pipeline repo though. If we indeed can fix:
than it wouldnt matter that much to me which variant of the pipeline proposal is chosen. But if those issues are too hard to fix, it may be another argument to take the elixer pipeline proposal more serious. |
PR adding pipeline operator support to TypeScript. The proposal is currently a stage 1 proposal for EcmaScript. It "introduces a new operator |> similar to F#, OCaml, Elixir, Elm, Julia, Hack, and LiveScript, as well as UNIX pipes and Haskell's &. It's a backwards-compatible way of streamlining chained function calls in a readable, functional manner, and provides a practical alternative to extending built-in prototypes."
Currently this is a minimal implementation of pipeline operator, but I'm planning on making a F# variant support and after that a Topic/Smart pipeline variant PRs.
Partial application is a good pair for minimal/F# pipeline operator and has it's own PR at #37973. I'm planning on making a separate draft PR that has both features in them.
Code status
This works minimally. The TS transformer generates valid JS and there's working type inference. There are many bugs, inconsistencies with specs, insufficient error messages currently. Most of failing tests are not included in this PR since Playground won't get generated with any failures and having faulty baselines is shady. I'm eagerly awaiting feedback, testing and even help with this PR. Even using and talking of these features will help advance them through TC39 proposal process!
I used @rbuckton 's work on pipelineStage1 branch as a guide.
Testing
Playground link
To test it in your project, add following to your package.json:
and then run
npm install
.PR state
Backlog
milestone (required)master
branchgulp runtests
locallyWhy a draft PR for stage 1 proposal?
The TypeScript officially follows EcmaScript / stage 3 proposals. However, I'm opening this draft pull request despite it being only stage 1. The partial application and pipeline proposals now are in a stage where they want people to test the features - write it in their code, find problems, test existing codebases. The usual way these would be done is through a Babel plugin, but since a few years ago, just a plain text editor with syntax highlighting is no longer the default JS development environment.
TypeScript is no longer just a build step. It's used by a significant portion of JS developers for all their development, even when prototyping new code. If the tool chain of these developers requires TypeScript, then a simple Babel plugin won't help. This means that writing TypeScript with those features is impossible without first-class support for them in TypeScript.
But then could we ask them to write just plain JS instead? It turns out that TypeScript is more and more part of normal JavaScript development process with Language Server Protocol pointing out errors in code and providing typing information. Having people encounter red squigglies and disappearing type information as soon as they use the new language feature will not count favourably to the user experience. I believe having TypeScript support for new syntax proposals as important or even more important than having a Babel plugin.
I was considering creating and upkeeping a separate fork, but that would just mean duplication of work. There's already a great infrastructure for TypeScript with playgrounds, CI, etc. With a draft PR, new features can more easily get more eyes, testers and feedback. There will eventually be issues and questions on new features for TypeScript and github.com/microsoft/TypeScript is where anyone would first look up info on partial application.
Of course, I'm not expecting merging until the proposal reaches stage 3.