Skip to content

Instantly share code, notes, and snippets.

@krisselden
Last active August 4, 2019 01:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save krisselden/20267a5b27ab3f268e9ca2cb1a7b58bd to your computer and use it in GitHub Desktop.
Save krisselden/20267a5b27ab3f268e9ca2cb1a7b58bd to your computer and use it in GitHub Desktop.
// requires strict mode
// set false to minimize out runtime checks
const DEBUG = true;
export const enum TagTypes {
A,
B,
// C
/* adding this will cause Tag and unreachable to Error
until it is added to TagMapping and the switch case handled */
/**
* @internal
*/
InternalA,
}
export interface TagBase<T extends TagTypes> {
// Generic allows interfaces to narrow in strict mode
// if (tag.type === TagTypes.B) tag.b()
type: T;
value(): string;
}
export interface TagA extends TagBase<TagTypes.A> {
a(): string;
}
export interface TagB extends TagBase<TagTypes.B> {
b(): string;
other(): string;
}
interface TagInternalA extends TagBase<TagTypes.InternalA> {
secret(): string;
}
export interface PublicTagMapping {
[TagTypes.A]: TagA;
[TagTypes.B]: TagB;
}
interface InternalTagMapping {
[TagTypes.InternalA]: TagInternalA;
}
export type PublicTagType = keyof PublicTagMapping;
export type PublicTag = PublicTagMapping[PublicTagType];
type TagMapping = PublicTagMapping & InternalTagMapping;
// will error when an unmapped entry is added to TagTypes
// until it is mapped
export type Tag = TagMapping[TagTypes];
type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends ((k: infer I) => void)
? I
: never;
type MonomorphicTag = UnionToIntersection<Tag>;
type MonomorphicTagType = MonomorphicTag["type"];
class TagImpl implements MonomorphicTag {
public static create<T extends TagTypes>(type: T): TagMapping[T] {
return new this(type as MonomorphicTagType);
}
private constructor(public type: MonomorphicTagType) {}
public a(this: TagA) {
if (DEBUG) {
// checkTagType(this, TagTypes.B)
// would Error because we declared this: TagA
// this way allows us to cleanly strip the check in minify
checkTag(this, TagTypes.A);
}
return "A";
}
public secret(this: TagInternalA) {
if (DEBUG) {
checkTag(this, TagTypes.InternalA);
}
return "Secret A";
}
public b(this: TagB) {
if (DEBUG) {
checkTag(this, TagTypes.B);
}
return this.other();
}
public other(this: TagB): string {
if (DEBUG) {
checkTag(this, TagTypes.B);
}
return "B";
}
public value(): string {
return value(this);
}
}
function checkTag<T extends TagTypes>(tag: TagMapping[T], type: T) {
if (!isTagType(tag, type)) {
throwError(tag, type);
}
}
function isTagType<T extends TagTypes>(
tag: Tag,
type: T,
): tag is TagMapping[T] {
return tag.type === type;
}
function throwError(tag: Tag, type: TagTypes): never {
throw new Error(`Expected tag to be of type ${type} but was ${tag.type}`);
}
function _createTag<T extends TagTypes>(type: T): TagMapping[T] {
return TagImpl.create(type);
}
export const createPublicTag: <T extends PublicTagType>(
type: T,
) => PublicTagMapping[T] = _createTag;
function value(tag: Tag) {
switch (tag.type) {
case TagTypes.A:
return tag.a();
case TagTypes.B:
return tag.b();
case TagTypes.InternalA:
return tag.secret();
default:
// if another value is added to enum TagTypes this errors
// because it will no longer reduce to never
return unreachable(tag);
}
}
function unreachable(tag: never): never {
throw new Error(`Unknown tag ${tag}`);
}
// type is TagA
const a = createPublicTag(TagTypes.A);
a.a();
// a.b(); // Errors
value(a);
// type is TagB
const b = createPublicTag(TagTypes.B);
b.b();
// b.a(); // Errors
value(b);
const internalTag = _createTag(TagTypes.InternalA);
internalTag.secret();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment