Skip to content

Commit db8979b

Browse files
Add TS Tagged Unions section
1 parent 06ce0ad commit db8979b

File tree

1 file changed

+76
-2
lines changed

1 file changed

+76
-2
lines changed

docs/blog/2023/2023-04-20-Better_Typed_than_Sorry.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ If you are a Fable user you may have noticed that we released, quite quietly, Fa
1414

1515
> If you haven't upgraded yet, we recommend you do it soon: there are no breaking changes and you [only need to update a few packages](../2022/2022-09-28-fable-4-theta.html#packages-updated-for-fable-4).
1616
17-
With Fable 4.1 we are happy to announce one of those targets is ready for prime-time! Thanks to the effort of [ncave](https://github.com/ncave) who has been leading the effort to add type annotations to JS output (in spite of my initial reluctance) for a long time, TypeScript compilation started to become a real possibility. We have been working together in the last month and TypeScript generated code is almost on par now with JS so we have decided to bump the status from beta to stable 🚀
17+
With Fable 4.1 we are happy to announce one of those targets is ready for prime-time! Thanks to the work of [ncave](https://github.com/ncave) who has been leading the effort to add type annotations to JS output (in spite of my initial reluctance) for a long time, TypeScript compilation started to become a real possibility. We have been working together in the last month and TypeScript generated code is almost on par now with JS so we have decided to bump the status from beta to stable 🚀
1818

1919
Wait a moment, compiling to TypeScript? Does this mean I should compile all my JS apps to TypeScript from now on for extra type-safety? No, we don't recommend that. In fact, you probably shouldn't. Many Fable libraries already emit JS code that TypeScript may not accept happily and we cannot control that. TypeScript compilation is intended for integrating F# into existing TypeScript and to write libraries that can be published to npm and consumed with a type-safe API. This is in line with the focus we are giving to the new language targets: instead of trying to do everything in F#, we will concentrate our efforts on having a great experience with [Domain Programming](https://fsharpforfunandprofit.com/ddd/) in all platforms. This means spending less time writing bindings for native libraries and instead working on generating nice code that can be easily consumed.
2020

@@ -82,7 +82,7 @@ c.line(data)
8282
})
8383
```
8484

85-
> A similar effect can be reached with [anonymous records](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/anonymous-records).
85+
> A similar effect can be achieved with [anonymous records](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/anonymous-records).
8686
8787
### Exporting members
8888

@@ -265,6 +265,80 @@ test(MyUnion_Bar(5))
265265
test(MyUnion_Baz())
266266
```
267267

268+
### TypeScript Tagged Unions
269+
270+
Even if F# unions can be used in a typed-safe manner from TypeScript, when you build a public API you may want your unions to feel more "native". And it happens that TypeScript does have the concept of a [discriminated union](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions): which is a union of JS objects where all of them share a common property that has the function of the "tag". Fable can compile F# unions to TypeScript tagged unions thanks to the `TypeScriptTaggedUnion` attribute. This was a feature contributed by [cannorin](https://github.com/cannorin) and since **Fable 4.1.3** it has been updated to support TypeScript annotations. You define the name of the tag property in the attribute, and the name of each case will become the value of the tag. It is also important that you use **named fields** for each case. Let's see an example:
271+
272+
```fsharp
273+
[<TypeScriptTaggedUnion("type")>]
274+
type Command =
275+
| Take of fromIndex: int * toIndex: int
276+
| Edit of text: string
277+
| Save
278+
279+
let test = function
280+
| Take(fromIndex, toIndex) -> printfn $"Taking from {fromIndex} to {toIndex}"
281+
| Edit(text) -> printfn $"New text: {text}"
282+
| Save -> printfn "Saving"
283+
284+
Take(5, 7) |> test
285+
```
286+
287+
In TypeScript it becomes:
288+
289+
```ts
290+
export type Command =
291+
| { type: "take", fromIndex: int32, toIndex: int32 }
292+
| { type: "edit", text: string }
293+
| { type: "save" }
294+
295+
export function test(arg: Command): void {
296+
switch (arg.type) {
297+
case "edit": {
298+
toConsole(`New text: ${arg.text}`);
299+
break;
300+
}
301+
case "save": {
302+
toConsole(printf("Saving"));
303+
break;
304+
}
305+
default: {
306+
toConsole(`Taking from ${arg.fromIndex} to ${arg.toIndex}`);
307+
}
308+
}
309+
}
310+
311+
test({
312+
type: "take",
313+
fromIndex: 5,
314+
toIndex: 7,
315+
});
316+
```
317+
318+
By default the case names will change to camel case, but as with `StringEnum` you can pass a `CaseRules` argument to the attribute to control this, as well as using `CompiledName` in a case:
319+
320+
```fsharp
321+
[<TypeScriptTaggedUnion("method", CaseRules.SnakeCaseAllCaps)>]
322+
type HttpOptions =
323+
| Get
324+
| Post of body: string
325+
| Put of body: string
326+
| [<CompiledName("HEAD")>] OnlyHeaders
327+
328+
// TypeScript:
329+
//
330+
// type HttpOptions =
331+
// | { method: "GET" }
332+
// | { method: "POST", body: string }
333+
// | { method: "PUT", body: string }
334+
// | { method: "HEAD" }
335+
```
336+
337+
338+
:::warning
339+
Types decorated with `TypeScriptTaggedUnion` have similar limitations to `Erase` and `StringEnum`, that is, because the type has no actual representation in the JS runtime, you cannot use reflection, do type testing or attach interfaces (you can still use instance and static members from F#). It is recommended to use the special attributes only to interact with native code and not in your everyday F# programming.
340+
:::
341+
268342
## Compilation options
269343

270344
In order to compile to TypeScript, you only need to pass the `--lang ts` option to Fable tool, but let's have a look at [the extra options in the compostjs example](https://github.com/alfonsogarciacaro/compost/blob/e783ed687bc19887f02aa0b071585a1a152c8956/package.json#L7-L11):

0 commit comments

Comments
 (0)