Skip to content

Instantly share code, notes, and snippets.

@richsilv
Last active June 17, 2022 13:49
Show Gist options
  • Save richsilv/bd1466107a88a35963417ccfc6ee4a69 to your computer and use it in GitHub Desktop.
Save richsilv/bd1466107a88a35963417ccfc6ee4a69 to your computer and use it in GitHub Desktop.
// 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