I eschew switch. The JS switch syntax feels particularly archaic next to the rest of the nice typelevel stuff in TS. Instead, we use many-arm if-elseif-else with predicate functions, which is a simpler and more general pattern, because it allows branching for groups of types instead of relying on an exhaustive list of concrete values in all cases.
Here’s what I mean:
// You can imagine each member of the union being an object
// with the specific attributes for that element
type OutlineEl = 'header' | 'nav' | 'footer' | 'article'
type InlineEl = 'strong' | 'em'
type CustomEl = 'div' | 'span'
type El = OutlineEl | InlineEl | CustomEl
// Making these correct by construction left as an exercise to the reader
function isOutlineElement(e: El): e is OutlineEl { return true }
function isInlineElement(e: El): e is InlineEl { return true }
function isCustomElement(e: El): e is CustomEl { return true }
function doSomethingWithElement(e: El) {
if (isInlineElement(e)) {
// Handle all inline elements in a general way
// With a switch statement, we'd need a label for every member of the union's
// discriminant repeated explicitly in source code.
return
} else if (isOutlineElement(e)) {
// handle outline elements in a general way
return
} else if (e === 'div') {
// We can still handle specific cases naturally
// specifically handle div
return
} else if (e === 'span') {
// specifically handle span
return
}
// And we still get type narrowing down to never for exhaustive case checking.
assertNever(e)
}
// Copied from the article.
function assertNever(x: never): never {
throw new Error("The impossible has happened:" + x);
}
Okay, okay, here’s the way to implement the type assertions in a way that’s correct by construction, with no stuttering or duplicating of any string literals:
const isOutlineElMap = {
header: true,
nav: true,
footer: true,
article: true,
} as const
const isInlineElMap = {
strong: true,
em: true,
} as const
const isCustomElMap = {
div: true,
span: true,
} as const
type OutlineEl = keyof typeof isOutlineElMap
type InlineEl = keyof typeof isInlineElMap
type CustomEl = keyof typeof isCustomElMap
type El = OutlineEl | InlineEl | CustomEl
function isOutlineEl(e: El): e is OutlineEl {
return e in isOutlineElMap
}
// Making a higher-order function to produce isInlineEl and
// isCustomEl from an isElMap left as an exercise to the reader.
I hope they add something like pattern matching to make checking through the cases of the ADT a bit less cumbersome. Given how much ceremony it takes to define and use an ADT, I sometimes wonder if I’m even doing the right thing.
The switch statement is kind of a weak pattern matching. It doesn’t allow you to match on the fields or extract field values easily. I still consider it a good tool for your toolbox, specially if you do Redux. In Redux, you have ADTs, either explicitly declared, or implicitly declared. I prefer explicit in terms of types, so at least the typechecker can spot some of my mistakes.
This is even better in F# (compiled to ECMAScript with Fable). I highly recommend evaluating Fable for your ES needs if you are currently using TypeScript but like this sort of thing.
type MyAdt =
| Option1 of fooField : string
| Option2 of barField : float * bazField : string
| Option3 of fooField : float * bazField : string
let doSomething (value : MyAdt) =
match value with
| Option1 (fooField) ->
printfn "Option 1 fields: %s" fooField
| Option2 (barField, bazField) ->
printfn "Option 2 fields: %f, %s" barField bazField
| Option3 (fooField, bazField) ->
printfn "Option 3 fields: %f, %s" fooField bazField
or, perhaps slightly less conveniently, with records:
type MyAdt =
| Option1 of
{|
fooField : string
|}
| Option2 of
{|
barField : float
bazField : string
|}
| Option3 of
{|
fooField : float
bazField : string
|}
let doSomething (value : MyAdt) =
match value with
| Option1 fields ->
printfn "Option 1 fields: %s" fields.fooField
| Option2 fields ->
printfn "Option 2 fields: %f, %s" fields.barField fields.bazField
| Option3 fields ->
printfn "Option 3 fields: %f, %s" fields.fooField fields.bazField
We don’t need the “impossible” case because the compiler (naturally) checks pattern matches for exhaustion.
I eschew switch. The JS switch syntax feels particularly archaic next to the rest of the nice typelevel stuff in TS. Instead, we use many-arm if-elseif-else with predicate functions, which is a simpler and more general pattern, because it allows branching for groups of types instead of relying on an exhaustive list of concrete values in all cases.
Here’s what I mean:
See it on Typescript Playground
Okay, okay, here’s the way to implement the type assertions in a way that’s correct by construction, with no stuttering or duplicating of any string literals:
See it on Typescript Playground
I hope they add something like pattern matching to make checking through the cases of the ADT a bit less cumbersome. Given how much ceremony it takes to define and use an ADT, I sometimes wonder if I’m even doing the right thing.
The switch statement is kind of a weak pattern matching. It doesn’t allow you to match on the fields or extract field values easily. I still consider it a good tool for your toolbox, specially if you do Redux. In Redux, you have ADTs, either explicitly declared, or implicitly declared. I prefer explicit in terms of types, so at least the typechecker can spot some of my mistakes.
This is even better in F# (compiled to ECMAScript with Fable). I highly recommend evaluating Fable for your ES needs if you are currently using TypeScript but like this sort of thing.
or, perhaps slightly less conveniently, with records:
We don’t need the “impossible” case because the compiler (naturally) checks pattern matches for exhaustion.
Worlds longest link to Fable playground with this code.
How does Fable compare to Reason/OCaml?