|
| 1 | +--- |
| 2 | +layout: fable-blog-page |
| 3 | +title: Glutinum, a new era for Fable bindings |
| 4 | +author: Mangel Maxime |
| 5 | +date: 2024-01-01 |
| 6 | +author_link: https://twitter.com/MangelMaxime |
| 7 | +author_image: https://github.com/MangelMaxime.png |
| 8 | +# external_link: |
| 9 | +abstract: | |
| 10 | + Let me introduce Glutinum, a new ecosystem for Fable bindings aiming to provide the best F# experience while staying close to the original JavaScript API. |
| 11 | +--- |
| 12 | + |
| 13 | +2 years ago, I soft-launched **Glutinum** project with the goal to push Fable bindings to the next level. |
| 14 | + |
| 15 | +Thanks to new innovations in Fable, and after working on 20+ bindings, I am now ready to say it is possible to provide near native F# experience while staying close to the original JavaScript API. |
| 16 | + |
| 17 | +The former allows F# developers to consume bindings with minimal friction. While the later makes it easier to re-use knowledge and documentation coming from the original JavaScript community. |
| 18 | + |
| 19 | +With this confirmation, I started prototyping a new tool to convert TypeScript definitions to F#. |
| 20 | + |
| 21 | +## Why a new tool? |
| 22 | + |
| 23 | +For a long time, I have been split between re-writing ts2fable or creating a new tool. In the end, I decided to create a new tool because my goal is not only to provide a new converter but also a completely new set of bindings for Fable. |
| 24 | + |
| 25 | +Creating a new ecosystem makes it possible to progressively migrate to the new bindings without breaking existing libraries. |
| 26 | + |
| 27 | +## How is it different from ts2fable? |
| 28 | + |
| 29 | +I don't want to go into too much details about the design decision behind Glutinum CLI, but here are some highlights. |
| 30 | + |
| 31 | +### Minimize erased union types |
| 32 | + |
| 33 | +To me, the first source of friction when consuming Fable bindings is the usage of erased union types (`U2`, `U3`, etc.). |
| 34 | + |
| 35 | +Glutinum CLI minimizes the usage of erased union types by 2 main ways: |
| 36 | + |
| 37 | +#### 1. Inline values when possible |
| 38 | + |
| 39 | +Example based on **enum inheritance**. |
| 40 | + |
| 41 | +<div class="has-text-centered mb-3"> |
| 42 | + |
| 43 | +**TypeScript** |
| 44 | + |
| 45 | +</div> |
| 46 | + |
| 47 | +```ts |
| 48 | +export type ColorA = |
| 49 | + | 'black' |
| 50 | + |
| 51 | +export type ColorB = |
| 52 | + | 'bgBlack' |
| 53 | + |
| 54 | +export type Color = ColorA | ColorB; |
| 55 | +``` |
| 56 | + |
| 57 | +<div class="columns is-multiline is-mobile" data-disable-copy-button> |
| 58 | +<div class="column is-6 has-text-centered"> |
| 59 | + |
| 60 | +**ts2fable** |
| 61 | + |
| 62 | +</div> |
| 63 | +<div class="column is-6 has-text-centered"> |
| 64 | + |
| 65 | +**Glutinum** |
| 66 | + |
| 67 | +</div> |
| 68 | +<div class="column is-6"> |
| 69 | + |
| 70 | +```fs |
| 71 | +[<StringEnum>] |
| 72 | +[<RequireQualifiedAccess>] |
| 73 | +type ColorA = |
| 74 | + | Black |
| 75 | +
|
| 76 | +[<StringEnum>] |
| 77 | +[<RequireQualifiedAccess>] |
| 78 | +type ColorB = |
| 79 | + | BgBlack |
| 80 | +
|
| 81 | +type Color = |
| 82 | + U2<ColorA, ColorB> |
| 83 | +``` |
| 84 | + |
| 85 | +</div> |
| 86 | +<div class="column is-6"> |
| 87 | + |
| 88 | +```fs |
| 89 | +[<RequireQualifiedAccess>] |
| 90 | +[<StringEnum(CaseRules.None)>] |
| 91 | +type ColorA = |
| 92 | + | black |
| 93 | +
|
| 94 | +[<RequireQualifiedAccess>] |
| 95 | +[<StringEnum(CaseRules.None)>] |
| 96 | +type ColorB = |
| 97 | + | bgBlack |
| 98 | +
|
| 99 | +[<RequireQualifiedAccess>] |
| 100 | +[<StringEnum(CaseRules.None)>] |
| 101 | +type Color = |
| 102 | + | black |
| 103 | + | bgBlack |
| 104 | +``` |
| 105 | + |
| 106 | +</div> |
| 107 | +</div> |
| 108 | + |
| 109 | +#### 2. Generate multiple overloads |
| 110 | + |
| 111 | +In JavaScript it is common for a function/method to accept different types for the same argument. In such case, we can avoid the union type by generating multiple overloads. |
| 112 | + |
| 113 | +**TypeScript** |
| 114 | + |
| 115 | +```ts |
| 116 | +export class Dayjs { |
| 117 | + locale(preset: string | ILocale): Dayjs; |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +<div data-disable-copy-button> |
| 122 | + |
| 123 | +**ts2fable** |
| 124 | + |
| 125 | +```fs |
| 126 | +type [<AllowNullLiteral>] Dayjs = |
| 127 | + abstract locale: preset: U2<string, ILocale> -> Dayjs |
| 128 | +``` |
| 129 | + |
| 130 | +**Glutinum** |
| 131 | + |
| 132 | +```fs |
| 133 | +[<AllowNullLiteral>] |
| 134 | +type Dayjs = |
| 135 | + abstract member locale: preset: ILocale -> Dayjs |
| 136 | + abstract member locale: preset: string -> Dayjs |
| 137 | +``` |
| 138 | + |
| 139 | +</div> |
| 140 | + |
| 141 | +### Increased understanding of TypeScript utilities |
| 142 | + |
| 143 | +TypeScript **loves** to offer utility types to their users, so they can avoid code duplication / have a more interwoven type system. |
| 144 | + |
| 145 | +Glutinum CLI tries to understand those utilities and generates the best possible F# representation. |
| 146 | + |
| 147 | +For example, in the following example, `ts2fable` will generate an erased type `KeyOf` to mimic the behavior of `keyof` in TypeScript. Glutinum will instead generate a string enums with the literal values coming from the interface properties names. |
| 148 | + |
| 149 | +<div class="has-text-centered mb-3"> |
| 150 | + |
| 151 | +**TypeScript** |
| 152 | + |
| 153 | +</div> |
| 154 | + |
| 155 | +```ts |
| 156 | +export interface Point { |
| 157 | + x: number; |
| 158 | + y: number; |
| 159 | +} |
| 160 | + |
| 161 | +type P = keyof Point; |
| 162 | +``` |
| 163 | + |
| 164 | +<div class="columns is-multiline is-mobile" data-disable-copy-button> |
| 165 | +<div class="column is-6 has-text-centered"> |
| 166 | + |
| 167 | +**ts2fable** |
| 168 | + |
| 169 | +</div> |
| 170 | +<div class="column is-6 has-text-centered"> |
| 171 | + |
| 172 | +**Glutinum** |
| 173 | + |
| 174 | +</div> |
| 175 | +<div class="column is-6"> |
| 176 | + |
| 177 | +```fs |
| 178 | +[<Erase>] |
| 179 | +type KeyOf<'T> = Key of string |
| 180 | +
|
| 181 | +type [<AllowNullLiteral>] Point = |
| 182 | + abstract x: float with get, set |
| 183 | + abstract y: float with get, set |
| 184 | +
|
| 185 | +type P = |
| 186 | + KeyOf<Point> |
| 187 | +``` |
| 188 | + |
| 189 | +</div> |
| 190 | +<div class="column is-6"> |
| 191 | + |
| 192 | +```fs |
| 193 | +[<AllowNullLiteral>] |
| 194 | +type Point = |
| 195 | + abstract member x: float with get, set |
| 196 | + abstract member y: float with get, set |
| 197 | +
|
| 198 | +[<RequireQualifiedAccess>] |
| 199 | +[<StringEnum(CaseRules.None)>] |
| 200 | +type P = |
| 201 | + | x |
| 202 | + | y |
| 203 | +``` |
| 204 | + |
| 205 | +</div> |
| 206 | +</div> |
| 207 | + |
| 208 | +Here is as list of others ideas I have in mind: |
| 209 | + |
| 210 | +- Benefit from F# ability to open `static type`, so we can have access to more situations where we can avoid erased union types. |
| 211 | +- Have a plugin interface to generate specific code |
| 212 | + - Generates Feliz DSL when a library returns a `ReactElement` |
| 213 | +- Generate named erased union instead of `U2`, `U3`, etc. allowing for a more declarative code. |
| 214 | +- and much more... |
| 215 | + |
| 216 | +## Can I try it? |
| 217 | + |
| 218 | +For sure! 🎉 |
| 219 | + |
| 220 | +The easiest way to get started is to use `npx` to run the CLI without installing it: |
| 221 | + |
| 222 | +```sh |
| 223 | +npx @glutinum/cli path/file.d.ts |
| 224 | +``` |
| 225 | + |
| 226 | +You can also install it locally: |
| 227 | + |
| 228 | +```sh |
| 229 | +npm install @glutinum/cli |
| 230 | +``` |
| 231 | +:::warning{title="Warning"} |
| 232 | +Glutinum CLI is still in early development, not all of TypeScript syntax or planned optimizations are supported yet. |
| 233 | + |
| 234 | +Currently, if the CLI encounters an unsupported syntax it will most likely crash. I am still working on making it more tolerant to unsupported syntax. |
| 235 | +::: |
| 236 | + |
| 237 | +I wish you all a happy new year and I hope you will follow me in this new adventure. 🎉 |
0 commit comments