practical-fp / union-types

A Typescript library for creating discriminating union types.
MIT License
70 stars 2 forks source link

Nominal types #3

Closed anilanar closed 2 years ago

anilanar commented 2 years ago

Just dropping a note here, that it's possible to have nominal types by using a phantom property readonly [someSymbol]: unique symbol and then type-casting in tag function when creating a variant.

i.e.

declare const nominal: unique symbol;

export interface Variant<Tag extends string = string, Value = undefined> {
    readonly [nominal]: unique symbol
    readonly tag: Tag
    readonly value: Value
}

export function tag(tag: string, value?: unknown): AnyVariant {
    return {
        tag,
        value,
    } as AnyVariant
}

That way, the only way to construct a variant would be through the constructors returned by impl and it wouldn't even use symbols at runtime.

It could be a different function called nominalImpl or it could be a function impl(nominal: boolean), with all relevant types getting an additional generic argument <Nominal = false>.

Not use if type nominal types is interesting for others too or not.

felixschorer commented 2 years ago

I see why nominal types are useful.

  1. However, this proposal doesn't actually implement nominal types as all variants will be using the same unique symbol. You would still be able to forge variants by calling the library functions.
  2. If you want to have nominal types, you can always do that on top of this library. There is no reason why it should be part of this very focused library.
felixschorer commented 2 years ago

To elaborate on my second point, I personally would use a pattern like this.

import { impl, Variant } from "@practical-fp/union-types"

declare const SHAPE: unique symbol

type Circle = { radius: number } & typeof SHAPE
type Square = { sideLength: number } & typeof SHAPE

type Shape =
    | Variant<"Circle", Circle>
    | Variant<"Square", Square>

const { Circle, Square } = impl<Shape>()

function parseShape(shape: { radius?: number, sideLength?: number }) {
  const { radius, sideLength } = shape
  if (radius !== undefined && sideLength === undefined) {
    return Circle({radius} as Circle)
  } else if (radius === undefined && sideLength !== undefined) {
    return Square({sideLength} as Square)
  } else {
    return undefined
  }
}

Playground link

anilanar commented 2 years ago

Alright thanks.