Algebraic data types are the preferred way of modeling state on the typescript side. And we prefer the following way of implementing ADTs:
import type { Variant } from 'AlgebraicDataTypes.ts'
/* First, we need to define the options. We can store
* any additional info for each variant together with its string-based "tag" (as in Elm custom types)
*/
type GeometryShape =
| Variant<
'Triangle',
{
base: number
height: number
}
>
| Variant<
'Rectangle',
{
width: number
height: number
}
>
| Variant<
'Circle',
{
radius: number
}
>
/* Function to work with the resulting union type. With `strictNullChecks` enabled for the compiler
* If one would comment out the last `case` in the example below, it would be fine for
* the switch statement. TypeScript would just complain about the functions return type
* which would become `number | undefined`. If that's for some reason a valid return type for this function,
* one might not notice that there are not all shapes checked (the check is not exhaustive).
*/
function square(shape: GeometryShape): number {
switch (shape.kind) {
case 'Triangle':
return (shape.base * shape.height) / 2
case 'Rectangle':
return shape.height * shape.width
/* TypeScript will not complain if some case statement is missing
*
* case 'Circle':
* return (Math.PI * shape.radius * shape.radius) / 2
*/
}
}
/* To ensure the switch statement checks all possible variants (union-exhaustiveness-checking)
* one could just add a default case with a function that accepts an argument of type `never`.
* https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-exhaustiveness-checking
*/
import { assertNever } from 'AlgebraicDataTypes.ts'
function square(shape: GeometryShape): number {
switch (shape.kind) {
case 'Triangle':
return (shape.base * shape.height) / 2
case 'Rectangle':
return shape.height * shape.width
case 'Circle':
return (Math.PI * shape.radius * shape.radius) / 2
default:
assertNever(shape)
}
}