1

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.

3
  • 1
    Sure, this is possible, but could you make the code a minimal reproducible example by giving some sort of definition for Parser? Especially where you show some calls to $S() so we can all verify that the suggested solution behaves as expected. Commented Mar 15, 2022 at 20:17
  • this works but like @jcalz suggested the definition of Parser could break it potentially playground Commented Mar 15, 2022 at 20:18
  • 1
    The "right way" to do this is this approach using mapped array/tuple types, but I do want to see a minimal reproducible example here. Commented Mar 15, 2022 at 20:20

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! This was very helpful. Bonus round... I also have a 'Choice' parser $C like so: ``` export function $C<A>(a: Parser<A>): Parser<A> export function $C<A, B>(a: Parser<A>, b: Parser<B>): Parser<A | B> export function $C<A, B, C>(a: Parser<A>, b: Parser<B>, c: Parser<C>): Parser<A | B | C> ... ``` Is there a similar pattern available to select the union of type values?
Uh, like this I guess. Note that followup questions really belong in their own post (or, if it was important, should have been in the original question), since comments are supposed to be ephemeral and SO questions are meant to be FAQs for future readers. (Plus nobody gets any of those sweet reputation points for comments)
Thanks, I've got a lot to learn about TypeScript but these answers have helped me onto the next level.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.