Skip to content

Instantly share code, notes, and snippets.

@SimonMeskens
Last active February 5, 2025 01:10
Show Gist options
  • Save SimonMeskens/1ffed50143636def6bc2cd5afd7e9650 to your computer and use it in GitHub Desktop.
Save SimonMeskens/1ffed50143636def6bc2cd5afd7e9650 to your computer and use it in GitHub Desktop.
Higher Kinded Types in TypeScript
// Copyright 2021 Simon Meskens
// Licensed under the MIT License
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided this license is
// preserved. This software is offered as-is, without any warranty.
// Functor
interface StaticFunctor<G> {
map<F extends Generic<G>, U>(
transform: (a: Params<F>[0]) => U,
mappable: F
): Generic<F, [U]>;
}
// Examples
const arrayFunctor: StaticFunctor<unknown[]> = {
map: (fn, fa) => {
return fa.map(fn);
}
};
const objectFunctor: StaticFunctor<object> = {
map: (fn, fa) => {
return fn(fa);
}
};
const nullableFunctor: StaticFunctor<object | null | undefined> = {
map: (fn, fa) => {
return fa == undefined ? fa : fn(fa);
}
};
const stringify = (x: number) => JSON.stringify(x);
const xs = arrayFunctor.map(stringify, [4, 2]); // xs: string[]
const x = objectFunctor.map(stringify, 42); // x: string
const xNull = nullableFunctor.map(stringify, null); // xNull: null
const xSome = nullableFunctor.map(stringify, 4 as number | undefined); // xSome: string | undefined
const functor: StaticFunctor<unknown | unknown[]> = {
map(fn, fa) {
return Array.isArray(fa)
? arrayFunctor.map(fn, fa)
: fa != undefined
? objectFunctor.map(fn, fa)
: nullableFunctor.map(fn, fa);
}
};
const ys = functor.map(stringify, [4, 2]); // ys: string[]
const y = functor.map(stringify, 42); // y: string
const yNull = functor.map(stringify, null); // yNull: null
const ySome = functor.map(stringify, 42 as number | undefined); // ySome: string | undefined
// Plumbing
interface TypeProps<T = unknown, Params extends ArrayLike<unknown> = never> {
array: {
infer: T extends Array<infer A> ? [A] : never;
construct: Params[0][];
};
null: {
infer: null extends T ? [never] : never;
construct: null;
};
undefined: {
infer: undefined extends T ? [never] : never;
construct: undefined;
};
object: {
infer: [NonNullable<T>];
construct: Params[0];
};
}
type Match<T> = T extends infer U
? (unknown extends U
? any
: TypeProps<U>[Exclude<
keyof TypeProps,
"object"
>]["infer"]) extends never
? "object"
: {
[Key in Exclude<keyof TypeProps, "object">]: TypeProps<T>[Key]["infer"] extends never
? never
: Key
}[Exclude<keyof TypeProps, "object">]
: never;
type Params<T> = TypeProps<T>[Match<T>]["infer"];
type Generic<T, Params extends ArrayLike<any> = ArrayLike<any>> = TypeProps<
T,
Params
>[Match<T>]["construct"];
// Test output
console.log("TAP version 13");
console.log("# TypeProps Proof of Concept");
console.log("1..8");
console.log(
xs[0] === "4" && xs[1] === "2" ? "ok 1 array map" : "not ok 1 array map"
);
console.log(x === "42" ? "ok 2 object map" : "not ok 2 object map");
console.log(xNull === null ? "ok 3 null map" : "not ok 3 null map");
console.log(xSome === "4" ? "ok 4 some map" : "not ok 4 some map");
console.log(
ys[0] === "4" && ys[1] === "2"
? "ok 5 generic array map"
: "not ok 5 generic array map"
);
console.log(
y === "42" ? "ok 6 generic object map" : "not ok 6 generic object map"
);
console.log(
yNull === null ? "ok 7 generic null map" : "not ok 7 generic null map"
);
console.log(
ySome === "42"
? "ok 8 generic nullable map"
: "not ok 8 generic nullable map"
);
// Copyright 2021 Simon Meskens
// Licensed under the MIT License
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided this license is
// preserved. This software is offered as-is, without any warranty.
// Examples
const arrayFunctor = {
map: (fn, fa) => {
return fa.map(fn);
}
};
const objectFunctor = {
map: (fn, fa) => {
return fn(fa);
}
};
const nullableFunctor = {
map: (fn, fa) => {
return fa == undefined ? fa : fn(fa);
}
};
const stringify = x => JSON.stringify(x);
const xs = arrayFunctor.map(stringify, [4, 2]); // xs: string[]
const x = objectFunctor.map(stringify, 42); // x: string
const xNull = nullableFunctor.map(stringify, null); // xNull: null
const xSome = nullableFunctor.map(stringify, 4 as number | undefined); // xSome: string | undefined
const functor = {
map(fn, fa) {
return Array.isArray(fa)
? arrayFunctor.map(fn, fa)
: fa != undefined
? objectFunctor.map(fn, fa)
: nullableFunctor.map(fn, fa);
}
};
const ys = functor.map(stringify, [4, 2]); // ys: string[]
const y = functor.map(stringify, 42); // y: string
const yNull = functor.map(stringify, null); // yNull: null
const ySome = functor.map(stringify, 42 as number | undefined); // ySome: string | undefined
// Test output
console.log("TAP version 13");
console.log("# TypeProps Proof of Concept");
console.log("1..8");
console.log(
xs[0] === "4" && xs[1] === "2" ? "ok 1 array map" : "not ok 1 array map"
);
console.log(x === "42" ? "ok 2 object map" : "not ok 2 object map");
console.log(xNull === null ? "ok 3 null map" : "not ok 3 null map");
console.log(xSome === "4" ? "ok 4 some map" : "not ok 4 some map");
console.log(
ys[0] === "4" && ys[1] === "2"
? "ok 5 generic array map"
: "not ok 5 generic array map"
);
console.log(
y === "42" ? "ok 6 generic object map" : "not ok 6 generic object map"
);
console.log(
yNull === null ? "ok 7 generic null map" : "not ok 7 generic null map"
);
console.log(
ySome === "42"
? "ok 8 generic nullable map"
: "not ok 8 generic nullable map"
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment