Home > other >  Rest parameters and parameterized types
Rest parameters and parameterized types

Time:03-16

Is there a better way to handle a mapping of type parameters in TypeScript?

export function $S(): Parser<[]>
export function $S<A>(fn: Parser<A>): Parser<[A]>
export function $S<A, B>(a: Parser<A>, b: Parser<B>): Parser<[A, B]>
export function $S<A, B, C>(a: Parser<A>, b: Parser<B>, c: Parser<C>): Parser<[A, B, C]>
export function $S<A, B, C, D>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>): Parser<[A, B, C, D]>
export function $S<A, B, C, D, E>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>, e: Parser<E>): Parser<[A, B, C, D, E]>
export function $S<A, B, C, D, E, F>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>, e: Parser<E>, f: Parser<F>): Parser<[A, B, C, D, E, F]>
export function $S<A, B, C, D, E, F, G>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>, e: Parser<E>, f: Parser<F>, g: Parser<G>): Parser<[A, B, C, D, E, F, G]>
export function $S<A, B, C, D, E, F, G, H>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>, e: Parser<E>, f: Parser<F>, g: Parser<G>, h: Parser<H>): Parser<[A, B, C, D, E, F, G, H]>
export function $S<A, B, C, D, E, F, G, H, I>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>, e: Parser<E>, f: Parser<F>, g: Parser<G>, h: Parser<H>, i: Parser<I>): Parser<[A, B, C, D, E, F, G, H, I]>
export function $S<A, B, C, D, E, F, G, H, I, J>(a: Parser<A>, b: Parser<B>, c: Parser<C>, d: Parser<D>, e: Parser<E>, f: Parser<F>, g: Parser<G>, h: Parser<H>, i: Parser<I>, j: Parser<J>): Parser<[A, B, C, D, E, F, G, H, I, J]>
export function $S(...terms: Parser<any>[]): Parser<any[]>

// Minimal definition of parser
interface ParseState {
  input: string
  pos: number
}

interface ParseResult<T> {
  nextPos: number,
  value: T,
}

interface Parser<T> {
  (state: ParseState): ParseResult<T> | undefined
}

Basically I have a "sequence" function that takes any number of arguments and I'd like to know if there is a way to automatically map each type parameter into the response signature other than needing to make an overload for each number of arguments. I've seen this pattern used before but it's 2022 so I'm hoping there is a better way.

Background info that may be relevant to the question: Each Parser is a function that takes the current state (input string, current position) and returns a ParseResult if it matches at that position or undefined if it does not match. The ParseResult has a value and the new position to advance the input state to.

CodePudding user response:

You can use tuple/array mapping like this:

declare function $S<T extends any[]>(
  ...terms: { [I in keyof T]: Parser<T[I]> }
): Parser<T>;

When you map a type over an array/tuple type T (as long as T is generic) like {[I in keyof T]: F<I>}, it will result in another array/tuple type, where only the numeric-like index keys I will actually be iterated (although there's a bug at microsoft/TypeScript#27995 which makes this a little harder for implementers, but that's not an issue here since Parser<XXX> allows any type for XXX in your example).

So here T is a single array/tuple-like type corresponding to the various type parameters in your example (e.g., T would be [A, B, C] for your three-argument call signature). And { [I in keyof T]: Parser<T[I]> } is another tuple type the same length as T where each element type has been wrapped in Parser<>. So if T is [string, number, boolean], then the mapped type is [Parser<string>, Parser<number>, Parser<boolean>].

Let's make sure this works:

declare const x: Parser<string>;
declare const y: Parser<number>;
declare const z: Parser<boolean>;
const xyz = $S(x, y, z);
// const xyz: Parser<[string, number, boolean]>

Looks good!

Playground link to code

  • Related