Skip to content

Instantly share code, notes, and snippets.

@denistakeda
Last active March 5, 2024 13:50
Show Gist options
  • Select an option

  • Save denistakeda/2835cbd414516d14d3f1678fbb6061ed to your computer and use it in GitHub Desktop.

Select an option

Save denistakeda/2835cbd414516d14d3f1678fbb6061ed to your computer and use it in GitHub Desktop.
/**
* This type should be used to generate the Algebraic Data Types
* in typescript.
* */
export type Variant<Kind, Shape> = Readonly<{ kind: Kind } & Shape>
// eslint-disable-next-line @typescript-eslint/ban-types
export type EmptyVariant<Kind> = Variant<Kind, {}>
/**
* See usage explanation in docs/code_patterns.md.
*/
export function assertNever(arg: never): never {
console.error('Unexpected object: ' + arg)
return arg
}
import type { Variant, assertNever } from 'AlgebraicDataTypes.ts'
/* Export type itself, but not individual variants, makes this type opaque
* and hard to change from the outside, which forces users to keep domain
* logic close to type definition
*/
export type GeometryShape =
| Variant<
'Triangle',
{
base: number
height: number
}
>
| Variant<
'Rectangle',
{
width: number
height: number
}
>
| Variant<
'Circle',
{
radius: number
}
>
// Functions that are exported considered to be public. They are sort of API for domain.
export 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)
}
}
// Functions that are not exported are considered to be "private".
// They can be used by the other functions of this module.
function increase(times: number, shape: GeometryShape): GeometryShape {
// ...
}

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)
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment