Last active
June 17, 2022 13:49
-
-
Save richsilv/bd1466107a88a35963417ccfc6ee4a69 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
// Utility types (required only once): | |
interface Phantom<T> { | |
__phantom: T; | |
} | |
// The idea is that you narrow the type you're going | |
// to use with a property which you're definitely not | |
// going to use, but that distinguishes this from | |
// any old object which matches the same base interface. | |
export type NewType<T, TagT> = T & Phantom<TagT>; | |
// **************************************************** | |
// An example base interface: | |
interface ThingBase { | |
readonly foo: number; | |
readonly bar: number; | |
} | |
// A non-reproduceable type to make your final type | |
// non-reproduceable (this is safer than using a string). | |
// Not exported! | |
const THING_SYMBOL: unique symbol = Symbol(); | |
// Your resultant type. | |
export type Thing = NewType<ThingBase, typeof THING_SYMBOL>; | |
// eslint-disable-next-line @typescript-eslint/no-redeclare | |
export namespace Thing { | |
// A "constructor" for your type, which encodes the | |
// required logic and casts to the new type. | |
// You'd use similar free-functions with casts for | |
// updates, etc. | |
export function makeThing(foo: number, bar: number): Thing { | |
if (foo <= bar) { | |
throw new Error("Foo must be greater than bar!"); | |
} | |
return { | |
foo, | |
bar, | |
} as Thing; | |
} | |
export updateFoo(thing: Thing, newFoo: number) { | |
return Thing.MakeThing(thing.foo, Math.min(newFoo, thing.bar)); | |
} | |
} | |
// A function that requires the new type. | |
function logThing(thing: Thing) { | |
// eslint-disable-next-line no-console | |
console.log(thing.foo, thing.bar); | |
} | |
logThing({ foo: 10, bar: 5 }); // TYPE ERROR | |
logThing({ foo: 10, bar: 5, __phantom: Symbol() } as const); // TYPE ERROR | |
// There is no way to pass type checking without either calling the | |
// "constructor": | |
logThing(Thing.makeThing(10, 5)); // FINE | |
// Or casting to the new type manually | |
logThing({} as Thing); // FINE :( | |
// **************************************************** | |
// But note that this is exactly as type-safe as a class: | |
class ThingClass { | |
private constructor(readonly foo: number, readonly bar: number) {} | |
public static makeThing(foo: number, bar: number): ThingClass { | |
if (foo <= bar) { | |
throw new Error("Foo must be greater than bar!"); | |
} | |
return new ThingClass(foo, bar); | |
} | |
public updateFoo(newFoo: number) { | |
return new ThingClass(newFoo, Math.min(newFoo, this.bar)); | |
} | |
} | |
// A function that requires the new type. | |
function logThingClass(thing: ThingClass) { | |
// eslint-disable-next-line no-console | |
console.log(thing.foo, thing.bar); | |
} | |
logThing({ foo: 10, bar: 5 }); // TYPE ERROR | |
logThingClass(ThingClass.makeThing(10, 5)); // FINE | |
logThingClass({} as ThingClass); // FINE :( |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment