Last active
August 4, 2019 01:07
-
-
Save krisselden/20267a5b27ab3f268e9ca2cb1a7b58bd to your computer and use it in GitHub Desktop.
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
// 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