Skip to content

Instantly share code, notes, and snippets.

@MonoidMusician
Created January 19, 2026 20:01
Show Gist options
  • Select an option

  • Save MonoidMusician/57533fc7dfd5ec7a5e90ad92b02a2c1e to your computer and use it in GitHub Desktop.

Select an option

Save MonoidMusician/57533fc7dfd5ec7a5e90ad92b02a2c1e to your computer and use it in GitHub Desktop.
type Parser<T> = (input: string) => [T, string] | (null | Error);
function pure<O>(o: O): Parser<O> { return i => [o, i] }
{
// This should reconstruct a tuple type
// (Is there a better way to do this? please?)
// (See below, apparently there is ... kinda ...)
type Reconstruct<Is extends Array<unknown>> = {
[k in keyof Is & number]: Is[k]
} & Array<unknown>;
// This adds `Parser<_>` into each tuple field
type Sequence<Is extends Array<unknown>> = {
[k in keyof Is & number]: Parser<Is[k]>
} & Array<Parser<unknown>>;
// N-ary apply (`<*>`/`liftA2`)
function apply<Is extends Array<unknown>, O>(
fn: (...input: Is) => O,
...parsers: Sequence<Is>
): Parser<O> { return () => null }
// Without the function it infers just fine
function sequence<Is extends Array<unknown>>(
...parsers: Sequence<Is>
): Parser<Is> { return () => null }
// Try to force `Is` to infer from `parsers` not `fn`...
function applyR<Is extends Array<unknown>, O>(
fn: (...input: Reconstruct<Is>) => O,
...parsers: Sequence<Is>
): Parser<O> { return () => null }
// The simple case works:
const ex0 = sequence(pure(1), pure("2")); // infers Parser<[number, string]>
// But if we introduce a function, it doesn't
const ex1 = apply(x => x, pure(1), pure("2")); // infers Parser<unknown>
// It takes `Is` from the function ...
const ex2 = apply((x: number, y: string) => x+y, pure(1), pure("2")); // infers Parser<string>
// (It does check it properly)
/*fails*/ const ex3 = apply((x: never, y: string) => x+y, pure(1), pure("2")); // fails, number != string
// If we introduce `Reconstruct` to delay inference ... it fails to typecheck
/*fails*/ const ex4 = applyR(x => x, pure(1), pure("2"));
// It looks like `Reconstruct` has the wrong length-variance somehow
const ex5: Reconstruct<[1, "2"]> = [1, "2"]; // works
/*fails*/ const ex6: Reconstruct<[1, "2"]> = [1, "2", 3]; // fails??
const ex7: Reconstruct<[1, "2"]> = [1]; // but this works?!?
const ex7_0 = ex7[0]; // 1 | "2"
const ex7_1 = ex7[1]; // 1 | "2"
const ex7_2 = ex7[2]; // 1 | "2"
}
// Okay, let's try again
// https://stackoverflow.com/questions/51672504/how-to-map-a-tuple-to-another-tuple-type-in-typescript-3-0
// Use `[...unknown[]]` and preserve the `length` field
{
// This should reconstruct a tuple type
type Reconstruct<Is extends [...unknown[]]> = {
[k in keyof Is & number]: Is[k]
} & [...unknown[]] & { length: Is["length"] };
// This adds `Parser<_>` into each tuple field
type Sequence<Is extends [...unknown[]]> = {
[k in keyof Is & number]: Parser<Is[k]>
} & [...Parser<unknown>[]] & { length: Is["length"] };
// N-ary apply (`<*>`/`liftA2`)
function apply<Is extends [...unknown[]], O>(
fn: (...input: Is) => O,
...parsers: Sequence<Is>
): Parser<O> { return () => null }
// Without the function it infers just fine
function sequence<Is extends [...unknown[]]>(
...parsers: Sequence<Is>
): Parser<Is> { return () => null }
// Try to force `Is` to infer from `parsers` not `fn`...
function applyR<Is extends [...unknown[]], O>(
fn: (...input: Reconstruct<Is>) => O,
...parsers: Sequence<Is>
): Parser<O> { return () => null }
// The simple case works:
const ex0 = sequence(pure(1), pure("2")); // infers Parser<[number, string]>
// But if we introduce a function, it doesn't
const ex1 = apply((x, y) => x, pure(1), pure("2")); // infers Parser<unknown>
// It takes `Is` from the function ...
const ex2 = apply((x: number, y: string) => x+y, pure(1), pure("2")); // infers Parser<string>
// (It does check it properly)
/*fails*/ const ex3 = apply((x: never, y: string) => x+y, pure(1), pure("2")); // fails, number != string
// If we introduce `Reconstruct` to delay inference ... it fails to typecheck
/*fails*/ const ex4 = applyR(x => x, pure(1), pure("2"));
// This `Reconstruct`
const ex5: Reconstruct<[1, "2"]> = [1, "2"]; // works
/*fails*/ const ex6: Reconstruct<[1, "2"]> = [1, "2", 3]; // fails??
/*fails*/ const ex7: Reconstruct<[1, "2"]> = [1]; // okay, now this fails too
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment