Skip to content

[TS/JS] making the emitted JS match the emitted TS ? #4951

Open

Description

@goswinr

I emit TS to then use tsc to make d.ts files for my code. But this creates functions for DUs that don't exist in the emitted JS.

type Foo = 
    |Bar
    |Baz

Compiled to TS emits

 ...

export function Foo_Bar() {
    return new Foo<0>(0, []);
}

export function Foo_Baz() {
    return new Foo<1>(1, []);
}
...

But those don't get emitted when compiling to JS.

Image

I suggest making the emitted JS match the emitted TS.

For now, I can't use TS only because of some other errors in the emitted TS (yes, I could dig into those, but the emitted js works fine)
I would like to have d.ts for the public surface of my JS. When just creating d.ts files tsc can jump over the other errors.

Activity

  1. shayanhabibi commented on Oct 25, 2026

    @shayanhabibi
    Contributor

    What if we use the information known by the compiler to emit the .d.ts directly with the javascript? That would allow us to emit string enums, and other erased structures on the type declaration side without having to actually emit correct .ts.

    Would also skip a build step for you.

  2. shayanhabibi commented on Oct 25, 2026

    @shayanhabibi
    Contributor

    Until Fable decides whether it wants something like this, you can use Fickl to write the .d.ts files straight from a JavaScript compilation, with no TypeScript output and no tsc step.

    dotnet tool install --global fickl --version 0.6.4
    fickl App.fsproj --declaration

    It takes the same arguments as fable. With --declaration, each .js file gets a .d.ts beside it. The declarations come from the same compilation: the file goes through the TypeScript code generator a second time, and only what the JavaScript actually exports is declared. So the union case helpers from this issue (Foo_Bar()), which only the TypeScript output has, never show up, and the .js is byte-for-byte what you get without the flag.

    For example:

    type Shape =
        | Circle of radius: float
        | Empty
    
    [<StringEnum>]
    type Color =
        | Red
        | [<CompiledName("dark-blue")>] DarkBlue
    
    type Point = { X: float; Y: float }
    
    let area shape =
        match shape with
        | Circle r -> System.Math.PI * r * r
        | Empty -> 0.0

    gives this Sample.d.ts (imports left out):

    export type Color = 
        | "red"
        | "dark-blue"
    export type Shape_$union = 
        | Shape<0>
        | Shape<1>
    export type Shape_$cases = {
        0: ["Circle", [float64]],
        1: ["Empty", []]
    }
    export declare class Shape<Tag extends keyof Shape_$cases> extends Union<Tag, Shape_$cases[Tag][0]> {
        constructor(tag: Tag, fields: Shape_$cases[Tag][1]);
        readonly tag: Tag;
        readonly fields: Shape_$cases[Tag][1];
        cases(): string[];
        static readonly Empty: Shape<1>;
    }
    export declare function Shape_$reflection(): TypeInfo;
    export declare class Point extends Record implements IEquatable<Point>, IComparable<Point> {
        readonly X: float64;
        readonly Y: float64;
        constructor(X: float64, Y: float64);
    }
    export declare function Point_$reflection(): TypeInfo;
    export declare function area(shape: Shape_$union): float64;

    Some details:

    • Erased, [<StringEnum>] and TypeScriptTaggedUnion unions are declared as type aliases, and files that only declare interfaces still get a .d.ts even though they have no JavaScript.
    • Class members are declared the way Fable compiles them to JavaScript: most F# members are module functions (Square__get_Side(sq)), and only interface implementations are methods.
    • fable-library-js ships with its own declarations, so the types imported from it (FSharpList, Option, int32, ...) resolve.
    • On Fable's JS test suite (75 files) every generated declaration type-checks with strict, apart from two cases that are the project's own: a [<Global>] class with no declaration anywhere, and an import of a hand-written .js file that has no types.

    If this looks useful for Fable itself, I'm happy to send it upstream. It's a separate library that runs over Fable's existing Fable AST and Babel AST, with a small hook in the CLI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions