Repository navigation
Feature Request: F# style Type Provider support? #3136
Description
Activity
saschanaz commented
on May 19, 2015 ContributorMore actions👍 with the example for HTML. I don't like writing many
declare vars to access them.- addedNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on May 19, 2015 The idea is definitely something we have talked about on and off over the past few years. We will need a detailed proposal of how are providers declared, implemented, loaded and discovered.
Reacted by Luke Deen TaylorI'm by no means any kind of expert on the TS compiler. I'm not even particularly advanced TS user right now. However, just to get the ball rolling I'll take a stab at a potential implementation for this.
There's obviously a bunch of different ways this could be done, and choosing the best way will depend on a deep understanding of TypeScripts goals as a language, how modern JS runtimes optimise code, among other things.
Here's a proposal for what the type provider might look like for the string formatter example:
(Please excuse the string formatting logic here, it's pretty much pseudo code. I didn't test and it's probably full of bugs.)//Type Provider definition StringF = @TypeProvider (str : String) => {//str eg: "id: %n/tname: %s" //Get array of format specifiers eg: ["s", "s", "n"] var formats = str.split("%") .map((str) => {str.substring(0,1);}) formats.splice(0, 1); //Convert formats to function arguments eg: [{name:"arg1", type: TP.baseTypes.String},{name:"arg2", type: TP.baseTypes.Number}] var funcParams = (() => { var types = formats.map((p) => { if (p === "s") {return TP.baseTypes.String;} else if (p === "n") {return TP.baseTypes.Number;} else if (p === "b") {return TP.baseTypes.Boolean;} else {throw p + " is not a valid string format specifier"} }) var args = [] for (var i = 0; i < types.length; i++) { args.push({ name: "arg" + i type: type }); } return args; })(); return new TP.Function({ parameters: funcParams //eg: [{name:"arg1", type: TP.baseTypes.String},{name:"arg2", type: TP.baseTypes.Number}] returnType: TP.baseTypes.String functionBody: @Expression """ var stringSplit = str.split("%"); for (var i = 1; i < stringSplit.length; i++) { stringSplit[i] = stringSplit[i].substring(1, stringSplit[i].length - 1); } for(var i = arguments.length - 1; i >= 0; i--){ stringSplit.insert(arguments[i], i); } return stringSplit.join(""); """ }) } //Type Provider being used console.log(StringF<"id: %n/tname: %s">(128, "battlebottle")) console.log(StringF<"id: %n/tname: %s">(128, "ben brown")) console.log(StringF<"id: %n/tname: %s/tage: %n">(128, "battlebottle", 30))In this implementation, a type provider is simply a function that gets annotated with @TypeProvider. Doing this means that this method must be evaluated at compile time. The type provider function may accept parameters. In this example the type provider accepts a
str : stringparameter. When the type provider is referenced in code, it must be passed this argument as a generic parameter, such as inStringF<"id: %n/tname: %s">,"id: %n/tname: %s"is the str parameter. This parameter needs to be a literal as it must be evaluated at compile time. With this parameter, the type provider function will generate and return an AST node to represent the object, function etc thatStringF<"id: %n/tname: %s">will represent. In this caseStringF<"id: %n/tname: %s">will represent a function with the signature(arg1 : number, arg2 : string) : string.As you can see, the TP.Function AST node constructor accepts an object with a
parametersfield, which must contain the name and type of all the parameters, areturnTypefield, which obviously declares the return type, and then afunctionBodyfield. This one I'm less certain about, but here I currently have this as a string containing TS code representing the function body. Marking this string with@Expressioncould help let IDE's know that this string contains TS, and should perhaps be validated. This is something of a pragmatic solution. In F# the equivalent of this would be writing<@@ 1 + 1 @@>or what have you, with<@@converting the expression inside into AST nodes. This is just syntactical sugar though for creating the AST nodes directly in code, and I don't think it does a whole lot for type safety. Evaluating JS string at run time is more normal in JS than doing something similar in C#/F#, and writing the function body as a string like this makes the whole process of creating a type provider a lot less complex than if the whole expression needed to be constructed with AST nodes. The TP.Function constructor defines the type signature of the function being created, which provides a layer of safety for when the type provider method is being evaluated, without making things feel as overwhelmingly complicated as writing F# type providers is today imo. I really have no idea what the best thing to do here. There's probably all kinds of things to consider here I haven't even thought about, but I'll leave this here as a starting point at least.The type provider method is evaluated at compile time for each time the type provider object is referenced with different parameters. So for example:
console.log(StringF<"id: %n/tname: %s">(128, "battlebottle")) console.log(StringF<"id: %n/tname: %s">(128, "ben brown")) console.log(StringF<"id: %n/tname: %s/tage: %n">(128, "battlebottle", 30))results in the type provider being evaluated twice, since two of these methods have the exact same parameter, they can refer to the same function.
The AST node that the type provider function returns, is converted to a real function (or anything else) at compile time for any code referencing the type provider, and these AST nodes are also used to generate the JS output.
So lastly, here is the JS that would be created from this code if we compiled it:
var StringF = function (str) { switch(str) { case "id: %n/tname: %s": return (function(arg1, arg2) { var stringSplit = str.split("%"); for (var i = 1; i < stringSplit.length; i++) { stringSplit[i] = stringSplit[i].substring(1, stringSplit[i].length - 1); } for(var i = arguments.length - 1; i >= 0; i--){ stringSplit.insert(arguments[i], i); } return stringSplit.join(""); }); case "id: %n/tname: %s/tage: %n": return (function(arg1, arg2, arg3) { var stringSplit = str.split("%"); for (var i = 1; i < stringSplit.length; i++) { stringSplit[i] = stringSplit[i].substring(1, stringSplit[i].length - 1); } for(var i = arguments.length - 1; i >= 0; i--){ stringSplit.insert(arguments[i], i); } return stringSplit.join(""); }); default: throw "Could not find match for parameter: " + str; } } console.log(StringF("id: %n/tname: %s")(128, "battlebottle")); console.log(StringF("id: %n/tname: %s")(128, "ben brown")); console.log(StringF("id: %n/tname: %s/tage: %n")(128, "battlebottle", 30));I've tried to keep to the TypeScript spirit of keeping things "just JavaScript", and making it so the JS output is still highly readable and could comfortably be used if the developer decided to drop TypeScript at some point.
As you can see
StringF<"id: %n/tname: %s">(128, "battlebottle")is compiled toStringF("id: %n/tname: %s")(128, "battlebottle"). This keeps it looking largely familiar to the original TS code. The StringF variable, which was originally a type provider, is now a function. WhenStringF("id: %n/tname: %s")is called, it matches"id: %n/tname: %s"to the function that was generated at compile time for that parameter. Assuming the JS is unmodified, there will always be a function to match for the str parameter passed. I think this keeps things pretty readable, and if a developer wishes to drop TypeScript they could easily refactor this code to make it more idiomatic.Reacted by Alex Leung, Julien Cavaleiro and A01yaType Providers for TypeScript would be awesome for something like Relay/GraphQL https://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html
and FalcorJS
https://www.youtube.com/watch?v=z8UgDZ4rXBUReacted by Maxime Quandalle, Tristan Zajonc, Guntars Ašmanis-Graham, Isao Yagi, juno suárez, stephen, Steve Sewell, Per, Thomas Jenkins, Kris Kaczor and 13 moreAnother approach is to have a custom
CompilerHostthat generatesfoo-swagger.d.tsdynamically (preferably using some API like the one requested in #9147 as opposed to string concat), and either persists it to disk or servicesreadFilerequests for it from memory.Reacted by Martin Johns, Isao Yagi, Ryan Cavanaugh, Eric Riese, andyrooger and nponeccopReacted by Peter PistoriusMartinJohns commented
on Jun 15, 2016 ContributorMore actionsThe solution that Arnav Singh (@Arnavion) proposes would also help with importing TS-foreign types in combination with webpack. A few use cases are written in #6615, and that issue is already a great change it doesn't go far enough (in my opinion). It would be great if there's a way to hook up the system and provide custom definitions for code like
import styles from './main.less';.Reacted by stephenIt would be great if there's a way to hook up the system and provide custom definitions for code like import styles from './main.less';.
Agree that something like this would be cool. But I freestyle : https://github.com/blakeembrey/free-style (keep it all in ts for reals).
My opinions on the matter : https://medium.com/@basarat/css-modules-are-not-the-solution-1235696863d6 🌹
MartinJohns commented
on Jun 15, 2016 ContributorMore actionsBasarat Ali Syed (@basarat) While that is definitely a possible alternative, I don't think it's for the better.
- By moving stylesheets to the JavaScript, you also delay rendering of the homepage. Commonly you reference the StyleSheet first and use pre-rendering, and reference the JavaScript last. That way the browser can immediately start rendering. By moving it to the JavaScript you delay rendering until the browser has downloaded the JavaScript (which takes longer because it also contains the application logic), the browser needs to parse and JIT the JavaScript and execute it.
- It makes development much more difficult for people who are skilled in CSS, but less so in JavaScript.
- You can't utilize existing tooling as well.
By moving stylesheets to the JavaScript, you also delay rendering of the homepage
Sorted by simply writing out the css https://github.com/blakeembrey/free-style#css-string and putting it in a style tag.
It makes development much more difficult for people who are skilled in CSS
The people I speak of use ReactJs. Also they don't like the cascade + super kick ass deep in a dom tree selectors anyways.
You can't utilize existing tooling as well.
All you needs is TypeScript + TypeScript tooling 🌹
For further discussion please mention me at the blog as I'd rather not hijack this thread. Also type providers are a cool idea and if people want them for css then 💯 x 💕
felixfbecker commented
on Aug 28, 2016 ContributorMore actionsThis would be nice for type declarations of javascript libraries that do dynamic stuff, like
Bluebird.promisifyAll()Reacted by Kris KaczorAny current discussion on this topic? JSON/REST have become the lingua franca of data communications. Having type provider supports would be an enormous boost in productivity for accessing the plethora of web api's! Is this still just waiting for specs on loading it and providing access?
Reacted by nponeccop7 remaining items
Thanks for mentioning my repository, Kagami Sascha Rosylight (@saschanaz) 😄
AFAIK, custom transformers cannot be used to add or modify type definition at compile time (I'd love to know how if they can!), i.e., they cannot be used to automatically provide the return type ofhtmlIdentifiers<"filepath">()in your snippet.
In my example, the return type ofenumerate<'green' | 'yellow' | 'red'>()should be{ green: 'green'; yellow: 'yellow'; red: 'red'; }just because the signature ofenumerateisfunction enumerate<T extends string>(): { [K in T]: K };(https://github.com/kimamula/ts-transformer-enumerate/blob/master/index.ts).
My custom transformer has nothing to do with this, but it is used to modify emitted JavaScript code so thatenumerateactually works as declared.saschanaz commented
on Apr 30, 2017 ContributorMore actionsAh, I see, thanks for the detail 😄
What about something like this?
Somewhere in Typescript
export type TypeProvider = Function & { TypeChecker?: Function, IntellisenseProvider?: Function }
directive.d.tsin Angularimport AngularTemplateType from './ng-template.tp' export interface Component extends Directive { template?: AngularTemplateType.Type }
ng-template.tp.ts(all type provider need to end with .tp.ts, like .d.ts, and do not emit code for .tp.ts)import * as Typescript from 'typescript' export type Type = string // ^ Fallback for old version that does not support Type Provider // AngularTemplateType.Type will be treat as a string const Type: Typescript.TypeProvider = function (everything) { return Typescript.TypeFlags.String } Type.TypeChecker = function (everything) { // Now impl type checker for angular template }
Or what about GraphQL
import graphql from './gql' async function getUser(id: number) { return await graphql` { user(id: ${id}) { name, age } } ` } getUser(2) // <- Type of this is Promise<{ name: any, age: any }>
gql.tsimport GraphQL from './gql.tp' export default function (s: TemplateStringsArray): Promise<GraphQL> { return ... }
gql.tp.tsimport * as Typescript from 'typescript' export type Type = object const Type: Typescript.TypeProvider = function (everything) { return Typescript.createTypeLiteralNode(...) } Type.TypeChecker = function (everything) { // Now impl validator for GraphQL }
corpix/ajv-pack-loadercould benefit tremendously, based on something likebcherny/json-schema-to-typescriptI just wanted to voice my support and desire for this feature. I think it’s potentially incredibly valuable based on how TypeScript is often used these days for rich web apps with Webpack, REST & GraphQL, and other kinds of static but non-JS types.
Wesley Wigham (@weswigham) what ever came of your prototype from a couple years back? Did it at least yield a proposed API that could spark further discussion? I hope the TS team seriously considers building out this potential feature, though it could also be an excellent community contribution for a bold person given a clear proposed plan.
Reacted by JongChan Choi (Rieul), Simon, Timm Preetz, Netto Farah, Gurdas Nijor, Christian Meunier, Davy Duperron, Julien Cavaleiro, Younes meliani, Jiayi Hu and 36 moreSo, speaking from experience, while technically you can make type providers work (and I have - I did have a functional prototype based on my extension model), a better solution (from a ux perspective) is triggered codegen. Rather than writing a library that generates the types from an API or some other file and injecting them directly into a compilation or language service, it's a way better development experience to generate actual code from the API or file. When that's done, you have a source file that actually exists and can be gone to with go-to-definition and inspected for deficiencies.
And I think a lot of people realize this (plus it already works today, since it doesn't need any specific hooks) - that's why there's projects for generating type declarations for things like json schemas and swagger specs.
Reacted by Timm Preetz, Liam Doran, Gregory Benner, Alexander Clausen, Michał Lytek, Andrei Xavier de Oliveira Calazans, Naor Ami, Peter Burns, David Rojas Camaggi, Muthu Kumar and 5 moreReacted by JC, joshjg, Jonas Budelmann, Trey Brisbane, gparmigiani, pierre, Robert Lynch and Andras SlemmerReacted by JongChan Choi (Rieul), Mihail Malo, Daisy Leigh Brenecki, Jack Works and Max Makarov+1
Reacted by Martin Johns, Andrei Xavier de Oliveira Calazans, csha, Rokas Muningis and Max Makarovif typescript has a powerful macro system:
gql!(` query user($id: String) { user(id: $id) { name, age } }`) // will emits code: gql` query user($id: String) { user(id: $id) { name, age } }` as GqlTemplate<{ variables?: {id?: string}, data?: {user?: {name?: any, age?: any}}}> // if this macro system support async code emit, then it can know more: {name?: any, age?: any} : GqlTemplate<{ variables?: {id?: string}, data?: {user?: {name: string, age: number}}}>Reacted by Brad Phelan, Max Makarov, Renato Ribeiro, Yoav Balasiano, Sean Newell, Casper Engelmann and Joshua PassosReacted by Daisy Leigh Brenecki, Renato Ribeiro and Joshua Passoswe have language service plugins that can augment the experience in something like a string literal or whatnot, but nothing that changes the fundamental type of an expression.
Did this change? I'm looking into implementing a prototype myself and I'm evaluating what an appropriate integration would look like.
Another use case is typesafe i18n messages ICUs. We have a distinct build step for this now, but it would be nicer if it could be hooked into the typescript project references and incremental builds mechanisms. Even using a webpack custom loader would run too late for the language service to use it.
Reacted by Cole Lawrence and Oliver Joseph AshType Providers could be used to support many of the same use-cases as the much requested Custom Transformers feature #14419
Reacted by Kirill Alexander Khalitov, Daisy Leigh Brenecki, sosoba, Cole Lawrence, Alexey Alekhin, Peter Pistorius, Max Makarov and Ilya BorisovAny updates?
Reacted by Ilya Borisov and Joshua J.This issue was opened before some things we have today existed. Nowadays, ES Module
import/exportsyntax is totally standard in the JavaScript community, and we also have JavaScript runtimes that allow us to import from any where using URLs, so we don't need to add this dynamic feature to the language.The Deno TypeScript/JavaScript runtime allows us to import using familiar
importsyntax from any server.For example, in Deno we can write the following:
import SwaggerClient from 'https://some-swagger-api.com' var fooClient = new SwaggerClient() fooClient.getPeople((people) => { people.every((person) => console.log(person.firstName + "," + person.lastName); });
In Node.js, it is simple to make a URL module loader. This means someone can make one that loads TypeScript in Node.js (go go go!).
I think this issue can be closed, and people should instead focus on building tooling around ES Module syntax.
Reacted by arontsangReacted by Ilya Borisov, Aron Adler, Joe Taber, Alex Leung, Oliver Rose, arontsang, Nils Bergmann, aymeric bouzy, Sean Newell and kpietraszkoI'm not clear that this is a reason to close it -
importsyntax doesn't really cover many of the code-generation-specific intentions and capabilities of type providers. Do you have an example that would cover cases such as generating types for a database target at design time based on server-side introspection queries?Reacted by Farzad Senart, Aron Adler, Alex Leung, aymeric bouzy and Jonathan HefnerJoe Pea (@trusktr): like Zoey (@Dessix), I think you miss the purpose of F#-style Type Providers. Type Providers are not just 'yet another way to import existing code'. Instead, they enable you to programmatically derive new types (that don't exist anywhere) at design time, which the rest of your program can then type check against without ever having them declared in a source file explicitly.
Reacted by Aron Adler, xialvjun, Naveen Michaud-Agrawal, arontsang, AvroraPolnareff, Hikurangi, Josh Junon, Nils Bergmann, Alexey Berezin, aymeric bouzy and 10 more
Could we get support for something like F#'s type providers in the future?
Let me present some use case scenarios for this.. Let's suppose you have a TS project trying to access a REST API, and this REST API uses Swagger or RAML for documentation. Well in this case we could have something like this;
and this could be completely type safe, as the SwaggerClient pulls down the API return types, method names (such as getPeople) and signatures at compile time and exposes them as methods to the type system. I think that's pretty huge. Such clients could be written by the community for RAML, SOAP or what have you.
another nice one:
In this example the Document type provider loads our index.html file and finds all the elements with id's and figures out what types they are, and exposes them in the type system at compile time. Again, I think that's pretty huge!
F# uses Type Providers for all kinds of things, so there's lots of ways this can get used. It's easy to see frameworks figuring out all kinds of amazing things that these could be used for. It also feels like a nice fit for TypeScript because it helps bridge the benefits on dynamic typing with the benefits of static typing.
one more tiny example:
a simple string formatter. This reads "name:%s/tid:%n" and knows that %s and %n must be replaced with a string and number respectively, so at compile time it produces a method that accepts a string parameter and a number parameter. Basically a statically typed string formatter. Just a tiny of example of handy little utilities that could be written with this feature. But the opportunity is pretty huge I think in general.
Any thoughts?