#35405•Medium

Higher-Order Function

Fix MapArray so a generic transform runs per tuple element. TypeScript has no higher-kinded types, so the transform has to travel as a type-level lambda.

This one starts from working code that returns the wrong types.

You get three higher-order functions whose inner functions are generic over the string they receive: toLowerCase() lowercases it, repeat(n) repeats it n times, concat(...parts) appends to it. mapArray(tuple, fn) should run one of those inner functions over every element and hand back a tuple of the transformed literals. The shipped implementation reaches for ReturnType:

type MapArray = <T extends unknown[], M extends (...args: any) => any>(t: T, m: M) => Mapped<T, M>
 
type Mapped<T extends unknown[], M extends (...args: any) => any> = { [I in keyof T]: ReturnType<M> }

ReturnType<M> has to produce one type for the whole signature, so it instantiates S with its constraint and throws the argument away. For the lowercasing function that means ReturnType<M> is Lowercase<string>, the same widened result for every slot, and mapArray(['Hello', 'World'] as const, toLowerCase()) comes back as [Lowercase<string>, Lowercase<string>] instead of ['hello', 'world']. Fixing it means giving the transform a form that Mapped can apply once per element.

Challenge Instructions: Higher-Order Function

Medium

Fix MapArray with higher-order functions.

This challenge involves implementing a type-safe MapArray utility that applies a higher-order function (HOF) to transform each element in a tuple. The provided implementation of MapArray does not work as expected. Your task is to fix it.

Why the provided solution fails:

  • The parameter S is scoped to the inner function returned by the HOF and cannot be dynamically bound from the context where MapArray applies the function

What you need to do:

  1. Fix each HOF type
  • Ensure that each higher-order function's parameter S and return type can be bound from MapArray
  1. Fix MapArray
  • Update the MapArray type to correctly bind S to the inner function returned by the HOF to each element of the tuple.

The test cases keep calling the inner functions directly as well, so the call signatures have to stay generic.

View on GitHub: https://tsch.js.org/35405

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

// Higher-Order Function: ToLowerCase
type ToLowerCase = () => <S extends string>(s: S) => Lowercase<S>;

// Higher-Order Function: Repeat
type Repeat = <N extends number>(count: N) => <S extends string>(s: S) => Repeated<N, S>;

type Repeated<N extends number | string, S extends string, O extends string = ''> = `${N}` extends `${infer L}${infer R}`
  ? Repeated<R, S, `${O}${O}${O}${O}${O}${O}${O}${O}${O}${O}${(['', S, `${S}${S}`, `${S}${S}${S}`, `${S}${S}${S}${S}`, `${S}${S}${S}${S}${S}`, `${S}${S}${S}${S}${S}${S}`, `${S}${S}${S}${S}${S}${S}${

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution, with the shared machinery first and one pair of types per higher-order function:

interface StringFn {
  arg: string
  out: string
}
 
type LambdaOf<F> = F extends { _fn?: infer L } ? NonNullable<L> : never
 
type Apply<F, A> = LambdaOf<F> extends infer L extends StringFn
  ? string extends A
    ? string
    : (L & { arg: A })['out']
  : never
 
interface ToLowerCaseFn extends StringFn {
  out: Lowercase<this['arg']>
}
 
interface LowerCaser {
  <S extends string>(s: S): Lowercase<S>
  readonly _fn?: ToLowerCaseFn
}
 
type ToLowerCase = () => LowerCaser
 
type Mapped<T extends unknown[], M extends (...args: any) => any> = {
  [I in keyof T]: Apply<M, T[I]>
}

Repeat and Concat follow the same two-part shape, with out: Repeated<N, this['arg']> and out: Concatenated<A, this['arg']>.

Why infer does not rescue the original

The reflex fix is to infer the return type at a known argument type:

type Apply<M, A> = M extends (arg: A) => infer R ? R : never
 
type Try = Apply<<S extends string>(s: S) => Lowercase<S>, 'Hello'>
// Lowercase<string>, not 'hello'

Conditional types do not call generic signatures. When TypeScript matches a generic source signature against a non-generic pattern it erases the type parameters to their constraints first, so S becomes string before the comparison and the literal is gone again. TypeScript has no way to say "this type is a function from types to types" directly, which is what a higher-kinded type would give you. So we encode one.

A type-level lambda

StringFn is that encoding. arg is a placeholder for the input and out is the body, written in terms of this['arg']:

interface ToLowerCaseFn extends StringFn {
  out: Lowercase<this['arg']>
}

Inside an interface, this is a real type parameter: it refers to whatever the interface ends up being once it is combined with something else. That is the whole trick. Intersect the lambda with an object that pins arg, and every mention of this['arg'] in out re-resolves against the intersection:

type Bound = (ToLowerCaseFn & { arg: 'Hello' })['out']
// 'hello'

arg: string & 'Hello' is 'Hello', so out evaluates to Lowercase<'Hello'>. The lambda has been applied without ever calling anything.

Carrying the lambda on the function type

The tests still call the inner functions directly, so toLowerCase()('TS') has to stay 'ts'. The call signature therefore stays generic, and the lambda rides along as an extra property:

interface LowerCaser {
  <S extends string>(s: S): Lowercase<S>
  readonly _fn?: ToLowerCaseFn
}

The property has to be optional. const toLowerCase: ToLowerCase = () => (s): any => s.toLowerCase() returns a plain arrow function with no such property, and a required member would reject it. Optionality costs one step on the type side: infer L picks up ToLowerCaseFn | undefined, which LambdaOf strips with NonNullable.

Mapping the tuple

Mapped is a homomorphic mapped type over T, so a tuple in gives a tuple out, with each slot handled on its own:

type Step1 = Apply<LowerCaser, 'Hello'> // 'hello'
type Step2 = Apply<Repeater<2>, 'hello'> // 'hellohello'

The guard string extends A ? string : ... handles the elements that carry no literal. When the input is plain string, Lowercase<string> stays unevaluated and Concatenated<['.', '?'], string> grows into `${string}.?`, neither of which is the string the tests ask for. There is nothing to transform without a literal, so the result stays string.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges