Last active
January 9, 2023 21:37
-
-
Save WorldSEnder/9adaeb667b09bb753f073df072af38f1 to your computer and use it in GitHub Desktop.
Higher kinded types in typescript
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
// The two main components are the interfaces | |
// Generic<T, Context> and GenericArg<"identifier"> | |
// Generic basically structurally replaces types in T that are GenericArg<S> | |
// for some `S extends keyof Context` with `Context[S]` | |
// See the test cases for specific uses. | |
// ====== TESTING | |
// Pass through for trivial types | |
type Test00 = Generic<number>; | |
let t00: Assert<Test00, number> = true; | |
type Test01 = Generic<string>; | |
let t01: Assert<Test01, string> = true; | |
type Test02 = Generic<void>; | |
let t02: Assert<Test02, void> = true; | |
enum TestEnum { Green = 0, Blue = "STR" }; | |
type Test03 = Generic<TestEnum>; | |
let t03: Assert<Test03, TestEnum> = true; | |
type Test04 = Generic<0 | 2 | "strconstrant">; | |
let t04: Assert<Test04, 0 | "strconstrant"> = true; | |
// Given a GenericArg<S> and a context {[K in S]: T} returns T | |
type Test1 = Generic<GenericArg<"foo">, { foo: string }>; | |
let t1: Assert<Test1, string> = true; | |
// Given an object type, replaces all generics in each property | |
// GenericModel<foo> is replaced by GenericModel<foo=string> | |
type GenericModel = { prop: GenericArg<"foo"> }; | |
type Test2 = Generic<GenericModel, { foo: string }>; | |
let t2: Assert<Test2, { prop: string }> = true; | |
// Homogeneous array types work fine | |
type Test3 = Generic<GenericModel[], { foo: string }>; | |
let t3: Assert<Test3, { prop: string }[]> = true; | |
// And types can be reused in other types. | |
// Here DerivedModel<bar> contains a GenericModel<foo=bar> | |
// A DerivedModel<bar=string> propagates into GenericModel<foo=bar=string> | |
type DerivedModel = { underlying: Generic<GenericModel, { foo: GenericArg<"bar"> }> }; | |
type Test4 = Generic<DerivedModel, { bar: string }>; | |
let t4: Assert<Test4, { underlying: { prop: string } }> = true; | |
// Even works with spread parameters! | |
type FunctionWithSpreadParams = (input1: GenericArg<"foo">, ...input2: GenericArg<"bar">[]) => GenericArg<"foo">; | |
type Test5 = Generic<FunctionWithSpreadParams, { foo: string, bar: number }>; | |
// let t5: Assert<Test5, (input1: string, ...input2: number[]) => string> = true; | |
let t5: Assert<Test5, typeof RestParametersErrorSymbol> = true; // :/ | |
// This is primarily because I couldn't get general tuple-types to work | |
type BadTupleType = [GenericArg<"foo">, ...GenericArg<"bar">[]]; | |
type Test6 = Generic<BadTupleType, { foo: string, bar: number }>; | |
// let t6: Assert<Test6, [string, ...number[]]> = true; | |
let t6: Assert<Test6, typeof RestParametersErrorSymbol> = true; // :/ | |
// An of course, object types in function parameters are working aswell :) | |
type GoodFunctionModel = (i: { input1: GenericArg<"foo">, input2: GenericArg<"bar"> }) => GenericArg<"foo">; | |
type Test7 = Generic<GoodFunctionModel, { foo: string, bar: number }>; | |
let t7: Assert<Test7, (i: { input1: string, input2: number }) => string> = true; | |
// More generic tests | |
type BiggerTupleModel = [GenericArg<"foo">, number, string, {bar: GenericArg<"bar">}, GenericArg<"foobar">[]]; | |
type Test8 = Generic<BiggerTupleModel, {foo: string, bar: {complicated: string}, foobar: number}>; | |
let t8: Assert<Test8, [string, number, string, {bar: {complicated: string}}, number[]]> = true; | |
// ===== Example usage | |
// Nevertheless, this shows how we can use this. | |
// Basically, the keyword is higher-kinded types - in a way that is pleasent to read and write | |
// Declare a functor instance. The generic parameter is called "result" | |
interface Functor<F> { | |
map<A, B>(functor: Generic<F, { result: A }>, mapping: (a: A) => B): Generic<F, { result: B }>; | |
} | |
type GenericArray = GenericArg<"result">[]; | |
const arrayFunctor = { | |
map<A, B>(arr: A[], mapping: (a: A) => B): B[] { | |
return arr.map(mapping); | |
} | |
} as Functor<GenericArray>; | |
type Maybe<T> = T | void; | |
const maybeFunctor = { | |
map<A, B>(x: Maybe<A>, mapping: (a: A) => B): Maybe<B> { | |
if (x === undefined || x === null) { | |
return x as void; | |
} | |
return mapping(x as A); | |
} | |
} as Functor<Maybe<GenericArg<"result">>>; | |
// Javascript of course does not support implicit arguments | |
// So you have to provide the functor instance yourself when you need it | |
function double<F>(functor: Functor<F>, structure: Generic<F, { result: number }>): Generic<F, { result: number }> { | |
return functor.map(structure, n => 2 * n); | |
} | |
console.log(double(arrayFunctor, [0, 1, 2, 3, 4])); // Prints [ 0, 2, 4, 6, 8 ] | |
console.log(double(maybeFunctor, undefined)); // Prints undefined | |
console.log(double(maybeFunctor, 15)); // Prints 30 | |
// ===== Heavy lifting done here | |
const GenericArgSymbol = Symbol("GenericArgumentSymbol"); | |
interface GenericArg<T extends string> { [GenericArgSymbol]: T } | |
const GenericNotPresentErrorSymbol = Symbol("Generic-Arg couldn't be replaced. Not present in the arguments"); | |
const RestParametersErrorSymbol = Symbol("can't infer rest parameters at the moment"); | |
const GenericErrorSymbol = Symbol("Generic-Arg couldn't be converted. assert failed"); | |
const ArrayErrorSymbol = Symbol("Head<T> is never, but T is not an actual array (infer U)[]. assert failed"); | |
const ObjectErrorSymbol = Symbol("object couldn't be converted. assert failed"); | |
const FunctionErrorSymbol = Symbol("function couldn't be converted. assert failed"); | |
const OtherErrorSymbol = Symbol("unknown error. assert failed"); | |
const UnreachableErrorSymbol = Symbol("thought unreachable. assert failed"); | |
const MoreThanOneWrappedRestErrorSymbol = Symbol("only the first type in argument to Tuple can be a WrappedRest. assert failed"); | |
const GenericWrappedSymbol = Symbol("GenericArgumentSymbol"); | |
class WrappedRest<T> { [GenericWrappedSymbol]: T }; | |
// If Tuple is actually an array U[], this results in never | |
// If Tuple is [H, ...] for some H, this results in H | |
type Head<Tuple extends any[]> = Tuple extends [infer H, ...any[]] ? H : never; | |
// BEWARE OF ACTUAL ARRAYS. If U[], then Tail<U[]> = U[]. Check with Head first | |
type Tail<Tuple extends any[]> = ((...t: Tuple) => void) extends ((h: any, ...rest: infer R) => void) ? R : never; | |
// Prepends Element before the existing tuple. | |
// If Tuple is an actual array, this results in a tuple with a rest parameter | |
type Unshift<Tuple extends any[], Element> = ((h: Element, ...t: Tuple) => void) extends (...t: infer R) => void ? R : never; | |
// Reverses an tuple. A WrappedRest<U> at the start or a rest parameter at the end will be converted into each other | |
type Reverse<Tuple extends any[]> = Reverse_<Tuple, []>; | |
type Reverse_<Tuple extends any[], R extends any[]> | |
= { | |
3: Tuple extends (infer U)[] | |
? Unshift<R, WrappedRest<U>> | |
: typeof ArrayErrorSymbol; // Guarded above of us | |
2: Head<Tuple> extends WrappedRest<infer U> | |
? R extends [] | |
? Reverse_<Tail<Tuple>, U[]> | |
: typeof MoreThanOneWrappedRestErrorSymbol | |
: typeof UnreachableErrorSymbol, // Guarded above of us | |
1: R, | |
0: Reverse_<Tail<Tuple>, Unshift<R, Head<Tuple>>> | |
} [ Tuple extends [] | |
? 1 | |
: Head<Tuple> extends WrappedRest<infer U> | |
? 2 | |
: Head<Tuple> extends never | |
? 3 | |
: 0 | |
]; | |
type ReverseGenericTup_<T extends any[], Arg extends object, R extends any[]> | |
= { | |
2: T extends (infer U)[] | |
? typeof RestParametersErrorSymbol // Unshift<R, WrappedRest<Generic<U, Arg>>> // if this would work :) | |
: typeof ArrayErrorSymbol, // Guarded above of us | |
1: R, | |
0: ReverseGenericTup_<Tail<T>, Arg, Unshift<R, Generic<Head<T>, Arg>>>; | |
} [ T extends [] | |
? 1 | |
: Head<T> extends never | |
? 2 | |
: 0 | |
]; | |
type GenericFun<T extends Function, Arg extends object> | |
= T extends (...rest: infer A) => infer R | |
? GenericTup<A, Arg> extends infer GA | |
? GA extends any[] | |
? (...rest: GA) => Generic<R, Arg> | |
: GA // propagate any error | |
: typeof UnreachableErrorSymbol // can't fail since GA is simply an alias | |
: typeof FunctionErrorSymbol; // infers A and R, might fail if T is not actually a function? | |
type GenericTup<T extends any[], Arg extends object> | |
= ReverseGenericTup_<T, Arg, []> extends infer H | |
? H extends any[] | |
? Reverse<H> | |
: H // propagate any error | |
: typeof UnreachableErrorSymbol; // H is simply an alias, it always infers | |
type AnyGenericArg = GenericArg<any>; | |
type AnyArray = any[]; | |
type AnyObject = object; | |
type AnyFunction = (...args: any[]) => any; | |
// see also https://github.com/Microsoft/TypeScript/issues/14174 | |
type Generic<T, Arg extends object = {}> | |
= { | |
gen: T extends GenericArg<infer S> | |
? S extends keyof Arg | |
? Arg[S] | |
: typeof GenericNotPresentErrorSymbol | |
: typeof GenericErrorSymbol; | |
arr: T extends AnyArray | |
? Head<T> extends never | |
? T extends (infer U)[] | |
? Generic<U, Arg>[] | |
: typeof ArrayErrorSymbol | |
: GenericTup<T, Arg> | |
: typeof UnreachableErrorSymbol; // Taken care above of us | |
fun: T extends AnyFunction ? GenericFun<T, Arg> : typeof FunctionErrorSymbol; | |
obj: T extends {} | |
? { [K in keyof T]: Generic<T[K], Arg> } | |
: typeof ObjectErrorSymbol; | |
trv: T; | |
els: typeof OtherErrorSymbol; | |
} [ T extends AnyGenericArg | |
? 'gen' | |
: T extends AnyArray | |
? 'arr' | |
: T extends AnyFunction | |
? 'fun' | |
: T extends AnyObject | |
? 'obj' | |
: T extends boolean | number | string | symbol | void | undefined | null | never | unknown | |
? 'trv' | |
: 'els' | |
]; | |
type Assert<A, B> = A extends B ? B extends A ? true : never : never | |
// type anyArr = any[]; | |
// type fakeAnyArr = { [K in keyof anyArr]: anyArr[K]; }; | |
// const isFakeReal: Assert<anyArr, fakeAnyArr> = true; | |
// type funcDecl1 = (...args: anyArr) => void; | |
// type funcDecl2 = fakeAnyArr extends [...infer U] ? (...args: [U]) => void : never; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment