Skip to content

Instantly share code, notes, and snippets.

@Willmo36
Last active January 27, 2022 05:32
Show Gist options
  • Save Willmo36/6295b5b0ddec3277ee3cf0684a0e89b3 to your computer and use it in GitHub Desktop.
Save Willmo36/6295b5b0ddec3277ee3cf0684a0e89b3 to your computer and use it in GitHub Desktop.
TypeScript - Generic pattern matching
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type Union<T extends string> = Record<T, string>;
type MatchHandlers<T extends string, P extends Union<T>, R> = P extends any ? Record<P[T], (p: P) => R> : never;
type DefaultedOrFullHandlers<T extends string, P extends Union<T>, R> =
| (Partial<MatchHandlers<T, P, R>> & { otherwise: () => R })
| UnionToIntersection<MatchHandlers<T, P, R>>;
/**
* Create a match function based on the given tag property.
* For TS unions, we must first know the tag property
* @param tagProp - The tag of the union to switch on
*
* @example
*
const match = makeMatcher('type');
type A = { type: 'A'; foo: number };
type B = { type: 'B'; bar: string };
type C = { type: 'C'; baz: Date };
type Promo = A | B | C;
// Ensure all cases are handled
const full = match<Promo, string>({
A: (a) => a.foo.toString(),
B: (b) => b.bar,
C: (c) => c.baz.toDateString(),
});
const full_result = full({ type: 'A', foo: 123 });
// Partial cases with fallback
const defaulted = match<Promo, string>({
otherwise: () => 'hello',
});
const defaulted_result = defaulted({ type: 'B', bar: 'runs otherwise case' });
//Partial cases with no fallback will not compile
const missingCase_no_compile = match<Promo, string>({
A: () => 'A',
B: () => 'B',
});
*/
export function makeMatcher<T extends string>(tagProp: T) {
return function match<P extends Union<T>, R>(handlers: DefaultedOrFullHandlers<T, P, R>) {
return (p: P): R => {
const h = handlers as MatchHandlers<T, P, R> & Record<'otherwise', () => R>;
const key: P[T] = p[tagProp];
const handler = h[key] ?? h.otherwise;
return handler(p);
};
};
}
export const matchTag = makeMatcher('tag');
export const matchType = makeMatcher('type');
export const matchFpts = makeMatcher('_tag');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment