Skip to content

Instantly share code, notes, and snippets.

@dmorosinotto
Last active November 4, 2022 23:00
Show Gist options
  • Save dmorosinotto/394ad458bd5352c3869981771baeaf1d to your computer and use it in GitHub Desktop.
Save dmorosinotto/394ad458bd5352c3869981771baeaf1d to your computer and use it in GitHub Desktop.
Simulate *nominal types* in TypeScript (aka Brand/Mesure/Unit/Unique/Opaque types)
const __base__: unique symbol = Symbol(); //SUPER TRICK TO DEFINE UNIQUE __base__ TO HOLD THE Type BECAUSE infer DON'T WORK IN inferValue
export type Unique<T, U extends symbol> = T & { readonly __unique: U } & { [__base__]: T };
export type inferValue<U> = U extends Unique<infer T, symbol> ? U[typeof __base__] : never;
export function cast<U extends Unique<unknown, symbol>>(value: inferValue<U>): U { return value as U; }
export function val<U extends Unique<unknown, symbol>>(value: U): inferValue<U> { return value as inferValue<U>; }
//SAMPLE USE CASES
declare const sEUR: unique symbol;
export type EUR = Unique<number, typeof sEUR>;
declare const sUSD: unique symbol;
export type USD = Unique<number, typeof sUSD>;
declare const sMAIL: unique symbol;
export type MAIL = Unique<string, typeof sMAIL>;
type i = inferValue<EUR> //number OK
type s = inferValue<MAIL> //string OK
type n = inferValue<string> //NEVER
type n1 = inferValue<{a: string, __unique: symbol}> //NEVER
type n2 = inferValue<{a: string, __unique: symbol, [__base__]: 123}> //NEVER
var e = cast<EUR>(5); //EUR
var u = cast<USD>(5); //USD
e=u; //ERROR
u=e; //ERROR
u=u+1; //ERROR
e=cast(e+2); //OK e=7 EUR
var r1 = e-2; //number
var v = val(e); //number
var m = cast<MAIL>("a@b.com"); //MAIL
var mE = cast<MAIL>(123); //ERROR expected string not number
var m1 = cast<MAIL>(“123”); //OK MAIL
var s2 = m.substring(1,2) //string
var d = val(m); //string
function eq(a: EUR, b: USD) { return a==b } //ERROR ALWAYS FALSE
function eqV(a: EUR, b: USD) { return val(a)==val(b) } //OK
function eqN(a: EUR, b: USD) { if(a===b) return true; } //ERROR ALWAYS FALSE
function add(a: EUR, b: USD): EUR { return cast<EUR>(a + b*1.02) } //OK EUR
function addE1(a: EUR, b: USD): EUR { return a + b*1.02 } //ERROR number -> EUR
function addE2(a: EUR, b: USD): EUR { return cast<USD>(a + b*1.02) } //ERROR USD->EUR
function addU1(a: EUR, b: USD): USD { return cast(a/1.02 + b) } //OK USD
function addU2(a: EUR, b: USD) { return cast<USD>(a/1.02 + b) } //OK USD
var x = add(e,u); //OK EUR
add(u,u); //ERROR not EUR
add(e,e); //ERROR not USD
add(u,e); //ERROR flipped EUR/USD
var x = addU2(e,u); //ERROR x is EUR can't assign USD
var y = addU2(e,u); //OK USD
function sum(a:number, b:number) { return a+b}
var z = sum(e,u) //OK auto infer base type
declare const sIDQuote: unique symbol;
type IDQuote = Unique<string, typeof sIDQuote>
export interface Quote {
id: IDQuote,
name: string,
price: number,
parentQuote?: IDQuote | null,
}
function findQuoteNO(id: string) { return "DONT USE string" }
function findQuoteOK(id: IDQuote) { return "OK use IDQuote" }
export function findQuoteBETTER(id: Quote['id']) { return "EVEN BETTER infer IDQuote from 'id' prop" }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment