Skip to content

Instantly share code, notes, and snippets.

@svieira
Created May 19, 2020 05:49
Show Gist options
  • Save svieira/3574c151cd26c452657663f4247c0d63 to your computer and use it in GitHub Desktop.
Save svieira/3574c151cd26c452657663f4247c0d63 to your computer and use it in GitHub Desktop.
The best nominal typing solution as of TS 3.9.x
export declare const nominalTag: unique symbol
type UniqueTypes = string | number | symbol | boolean
/**
* Needs no validation nominal types
* (allows the base type T to be uplifted without a cast)
*/
export type Unique<T, M extends UniqueTypes> = T & { [nominalTag]?: M }
/**
* Validated nominal type
* (requires the base type T to be uplifted with a cast, ideally after validation)
*/
export type Validated<T, M extends UniqueTypes> = T & { [nominalTag]: M }
// Usage
// Declared enums used here, but any literal type would work (strings, numbers, unique symbols)
declare enum IdType { }
declare enum AccountBalanceType { }
type Id = Unique<number, IdType>
type AccountBalance = Unique<number, AccountBalanceType>
let id: Id = 123456789; // Raw values of type T are assignable to the Unique subtype
let balance: AccountBalance = 789;
id = balance; // Different types are not equivalent
// ERROR: Type 'Unique<number, AccountBalanceType>' is not assignable to type 'Unique<number, IdType>'
// Translations between Unique and Validated
type ValidatedId = Validated<number, IdType>
let validId: ValidatedId = 123 // Raw values of type T not assignable to validated
// ERROR: Type '123' is not assignable to type 'Validated<number, IdType>'.
validId = id; // Unique not assignable to Validated
// ERROR: Type 'Unique<number, IdType>' is not assignable to type 'Validated<number, IdType>'
validId = 12345 as ValidatedId // Casts "validate" as expected
id = validId // And validated types are exchangable with their "unique" subtypes as expected
// validId. // Nominal tag does not show up in the dotted auto-complete
// Use private fields for class nominality instead of Unique (doesn't work at all)
// or Validated(works but requires boilerplate)
class Point {
private [nominalTag]: never;
constructor(public x: number, public y: number){}
}
class Foo {
private [nominalTag]: never;
constructor(public x: number, public y: number){}
}
var x: Point = new Point(1, 2);
var y: Foo = new Foo(1, 2);
x = y;
// ERROR: Types have separate declarations of a private property '[nominalTag]'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment