Last active
November 4, 2022 23:00
-
-
Save dmorosinotto/394ad458bd5352c3869981771baeaf1d to your computer and use it in GitHub Desktop.
Simulate *nominal types* in TypeScript (aka Brand/Mesure/Unit/Unique/Opaque types)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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