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

Created on 19 Jul 2016  路  19Comments  路  Source: fable-compiler/Fable

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.

feature

Most helpful comment

fable-compiler 0.5.0 already outputs d.ts with the --declaration argument! :tada: 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.

All 19 comments

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 :wink: 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.

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 :wink: 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...

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.

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)

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 :+1:

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.

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.

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?

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

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 :)

@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.

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

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.

fable-compiler 0.5.0 already outputs d.ts with the --declaration argument! :tada: 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.

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?

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 :+1:

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

@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!

Works perfectly, Cheers!

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 :+1:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

forki picture forki  路  3Comments

tomcl picture tomcl  路  4Comments

ncave picture ncave  路  3Comments

et1975 picture et1975  路  3Comments

MangelMaxime picture MangelMaxime  路  3Comments