Skip to content

Instantly share code, notes, and snippets.

@donabrams
Last active August 3, 2018 18:29
Show Gist options
  • Save donabrams/74075e89d10db446005abe7b1e7d9481 to your computer and use it in GitHub Desktop.
Save donabrams/74075e89d10db446005abe7b1e7d9481 to your computer and use it in GitHub Desktop.
Nominal typing in TypeScript
interface FooId extends String {
_fooIdBrand: string;
};
export interface Foo {
id: FooId;
color: 'yellow';
isAwesome: true;
}
interface BarId extends String {
_barIdBrand: string;
};
export interface Bar {
id: BarId;
color: 'blue';
isAwesome: false;
}
type BeepId = FooId | BarId;
type Beep = Foo | Bar;
type GetId<T> = T extends { id: infer U } ? U : never;
function lookupById<T extends Beep>(ex: Beep[], id: GetId<T>): T | null {
return ex.find(({ id }) => id === id) as T | null;
}
interface PositiveTestResult {
foo: Foo | null;
bar: Bar | null;
beep: Beep | null;
}
function positiveTest(ex: Beep[]): PositiveTestResult {
const idFoo: FooId = "1" as any;
const idBar: BarId = "2" as any;
const idBeep: BeepId = "3" as any;
return {
foo: lookupById(ex, idFoo),
bar: lookupById(ex, idBar),
beep: lookupById(ex, idBeep),
};
}
interface NegativeTestResult {
foo: Foo | null;
bar: Bar | null;
foo2: Foo | null;
}
function negativeTest1(ex: Beep[]): NegativeTestResult {
const idFoo: FooId = "1" as any;
const idBar: BarId = "2" as any;
const idBeep: BeepId = "3" as any;
return {
foo: lookupById(ex, idFoo),
bar: lookupById(ex, idBar),
// This correctly fails to typecheck
foo2: lookupById(ex, idBeep),
};
}
function negativeTest2(ex: Beep[]): NegativeTestResult {
const idFoo: FooId = "1" as any;
const idBar: BarId = "2" as any;
const idBeep: BeepId = "3" as any;
const foo2 = lookupById(ex, idBeep);
return {
foo: lookupById(ex, idFoo),
bar: lookupById(ex, idBar),
// This correctly fails to typecheck
foo2,
};
}
function negativeTest3(ex: Beep[]): NegativeTestResult {
const idFoo: FooId = "1" as any;
const idBar: BarId = "2" as any;
const idBeep: BeepId = "3" as any;
// This correctly fails to typecheck
const foo2: Foo | null = lookupById(ex, idBeep);
return {
foo: lookupById(ex, idFoo),
bar: lookupById(ex, idBar),
foo2,
};
}
function negativeTest4(ex: Beep[]): NegativeTestResult {
const idFoo: FooId = "1" as any;
const idBar: BarId = "2" as any;
const idBeep: BeepId = "3" as any;
return {
foo: <Foo | null>lookupById(ex, idFoo),
bar: <Bar | null>lookupById(ex, idBar),
// This correctly fails to typecheck
foo2: <Beep | null>lookupById(ex, idBeep),
};
}
const testId: BeepId = "3" as any;
const test = lookupById([] as Beep[], testId);
function test2(a: Foo | null): string {
return a ? a.color : "clear";
}
// This correctly fails to typecheck
test2(test);
// How to force nominal types in Typescript (as opposed to duck types)
// From: https://github.com/Microsoft/TypeScript/issues/202#issuecomment-302402671
declare class NominalType<S extends string> {
private DO_NOT_DEFINE_ME_HUMAN: S;
}
type Email = string & NominalType<'Email'>;
type CustomerId = number & NominalType<'CustomerId'>;
const email = "yay";
function sendEmail(email: Email) {
console.log(email);
}
function cleanEmail(userInput: string): Email {
// TODO: clean up email
return userInput as Email;
}
sendEmail(email);
sendEmail(cleanEmail(email));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment