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

Can Fable generate .d.ts TypeScript declarations for generated code? #270

Closed
panesofglass opened this issue Jul 19, 2016 · 19 comments
Closed
Labels

Comments

@panesofglass
Copy link

Description

I have a mature TypeScript codebase and would like to introduce Fable. In order to call Fable-generated code, I would like to have .d.ts files for the generated Fable code.

@alfonsogarciacaro
Copy link
Member

This feature has already been requested but I haven't had the time yet to implement it, I'll try to do it in the next few weeks. Having it as a pending issue will make for a nice reminder 😉 Note that the fable-core library has been recently converted to TypeScript so you can already make use of the methods and classes implemented there.

@tpetricek
Copy link
Collaborator

I think building a prototype for this would be a fun project that someone from the community could start working on.

In case someone wanted to send a PR 😉 the Fable compiler already walks over the typed AST from F# (I guess this is happening here), so the change would just have to do the same thing and turn the type info into a .d.ts file...

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Jul 20, 2016

I'd love if someone could do do that but maybe it's a bit complicated for someone without a deep understanding of how F# and Fable AST work ;)

I think it shouldn't be too complicated but I still need to have look at it. I was thinking that maybe it's easier to generate the .d.ts not in the F# to Fable step, but in the Fable to Babel one. There are some transformations that are done in the first step (for example, in F# AST class methods are children of the enclosing module not the class) that can be a bit difficult to handle.

@fdcastel
Copy link
Contributor

Well. It's a rainy weekend around here ;)

#277 adds a very-very-very-first implementation of this. As it stands now, it's unusable for any practical purposes. But enough to raise some questions.

Using it to compile the current integration-typescript sample produces the following util.d.ts:

import { List } from 'fable-core';

export function reverse(s: string): string;
export function greet(s: string): void;
export function sum(list: List<any>): number;

Open questions / problems to solve / todo list:

  • output: One .d.ts file per source or one giant index.d.ts?
    • e.g.: integration-typescript/node_modules/@types/node/index.d.ts
  • output file location: output file is being created at fable project root folder (ignores --outDir)
  • classes, interfaces, modules, namespaces
  • imports from fable-core (hardcoded by now)
  • mapping types from F# -> TypeScript (primitives, function signatures, generics)
  • compiler option? (--declaration is the one used by tsc)

@alfonsogarciacaro
Copy link
Member

Fantastic work! 👏 I need to have a deeper look later. For now, I'll try to answer your questions:

  • output: I think a single index.d.ts will be simpler both for us and the consumer.
  • output file location: I'd put it in outDir, but anyways the file can be moved later by the JS code (fable.js).
  • classes, interfaces, modules, namespaces: Classes should be quite straightforward. Interfaces are more complicated because they're erased in the generated code (for now, I would ignore them and solve that later, F# code often uses concrete types anyways). Namespaces usually disappear in the generated code and most modules become the module file itself. I guess we need to worry mainly about nested modules.
  • imports from fable-core: Not only from fable-core but also from any other external source. I need to think about this.
  • mapping types from F# -> TypeScript: primitives should be easy, function signatures will be trickier because Fable AST doesn't include this information (we may need to add it) and generics can become a real pain (or may be not, I need to check that better) so I'd ignore them just for now.
  • compiler option?: --declaration works for me 👍

@fdcastel
Copy link
Contributor

Another push to #277. Adds initial class declaration (very simple cases, far from complete).

I did notice the problem with interfaces. Maybe the code is too "late" in the transformation pipeline? But I'll wait for your considerations.

Some more problems:

  • Tuples appears as arrays
  • Enums appears as empty classes (uh?)
  • Record constructor arguments appears as $arg0, $arg1...

Moving the code to an earlier step in the compilation process maybe would help. But it will probably require some deeper changes (embedding it in transformMemberDecl ?).

Also, I did some tests trying to output one single index.d.ts file but I'm afraid it will be a very awkward solution (and probably impossible to reconcile when targeting ES2015 modules). After all, we are generating one .js file per module (util.js in our sample). And, in this scenario, the most intuitive way to import a module for a TypeScript user would be

import * as Util from './util';
import * as AnotherModule from './anothermodule';

instead of

import { Util } from './my-giant-declaration-file';
import { AnotherModule } from './my-giant-declaration-file';

In the first case, every .d.ts file is its own module. In the second one, we have one single .d.ts file in the format:

declare module Util {
    export ...
}

declare module AnotherModule {
    export ...
}

I will be glad to hear from more experienced TypeScript users about this subject.

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Jul 25, 2016

Personally I would start with something very simple to make it usable from TypeScript (even if it's just listing the method names) and use any wherever type info is not available. Then we can refine the declarations step by step.

I need to check better how TypeScript discovers the declarations (in the last case, the user can just include a /// reference), but I still think a single file would be more manageable. This shouldn't change the way modules are imported. Following your example, I guess it will be something like this:

import * as Util from './util';
import * as AnotherModule from './anothermodule';

In index.d.ts:

declare module './util' {
    export ...
}

declare module './anothermodule' {
    export ...
}

Check this declaration for example.

@panesofglass
Copy link
Author

A single *.d.ts will imply a single output file. I think you will need to generate 1:1 a *.d.ts to the generated *.js files. Does Fable allow you to merge out output files into a single *.js?

@alfonsogarciacaro
Copy link
Member

No, but it can be done with a bundler like Webpack.

Anyways, as mentioned above, if I'm not mistaken a single .d.ts file can indeed contain definitions for several ES6 modules (aka files). There're many examples of this, like the one below:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/material-ui/material-ui.d.ts

@alexswan10k
Copy link
Contributor

Whatever you guys decide as to if use 1:1 or 1:many, I would recommend taking a look at the official line on typings for npm packages.

My understanding is that the best way to handle this is with 1 per file (1:1), and to simply let the resolver sort out the rest. This prevents you needing to use named exports, which are brittle and incompatible with non-aliased paths.

I appreciate that people are not necessarily using npm for distributing their transpiled F# packages, but if it is of interest, I did create a really dumb typescript sample here which demonstrates how to package up .d.ts files with your node module. No more tsd yay :)

@fdcastel
Copy link
Contributor

@alfonsogarciacaro: Yes. A single .d.ts file can contain several modules. However, in this format it needs to be referenced using /// references, which is essentially a "legacy" way to integrate old .js code with TypeScript.

Using a 1:1 .d.ts mapping allows you to use the module in a more "natural" way, just importing the fable-generated code as if it were any other ES2015 module.

@Metal10k: Yes. Latest fable-core releases are already aligned with these guidelines. Look at the current integration-typescript sample and see how you can just import ... from 'fable-core' directly from the installed npm package.

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Jul 26, 2016

Ok, if everybody thinks one declaration per file is the way to go we should do that 👍

I've been investigating more about this, and probably it's better to focus our efforts to improve Fable and Babel AST and add type annotations to the generated code. Then use the Babel DTS generator to create the declarations from the annotated JS code.

Unfortunately, type annotations don't appear in Babel AST spec. However, Babel actually accepts them as it can be checked in the AST explorer.

@alfonsogarciacaro
Copy link
Member

fable-compiler 0.5.0 already outputs d.ts with the --declaration argument! 🎉 Please note this is still experimental and it's not perfect, particularly there's no type information emitted at the moment for interfaces and nested modules, I'll try to add this in next releases.

Please give it a try and tell me what you think.

@alexswan10k
Copy link
Contributor

This is pretty awesome :)

One minor issue i found with 0.5.1 is union types seem to catastrophically break.

Start compilation... ERROR: D:/Sandbox/test/Test.js: Missing class properties transform. (This is an error on an internal node. Probably an internal error)
Would you like me to create a ticket?

@alfonsogarciacaro
Copy link
Member

Damn, this is node again finding the plugin somewhere else in my computer and confusing me. This only happens if you pass the --declaration compiler argument, right?

Don't worry about the ticket, it's a very easy one. So I'll include it with other minor fixes 👍

@alexswan10k
Copy link
Contributor

Yes only when --declaration is used, otherwise it compiles fine. Cool, nice one 👍

@alfonsogarciacaro
Copy link
Member

@Metal10k, I've released [email protected] adding the babel-transform-class-properties plugin. Could you please give it a try to see if it works now? Thanks in advance!

@alexswan10k
Copy link
Contributor

Works perfectly, Cheers!

@alfonsogarciacaro
Copy link
Member

Thanks for the confirmation! Any comments about the process of generating and using the declarations are welcome (though please note that, as commented above, there are still a few things missing). It'd be also great if you could write a post explaining users about the new feature 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants