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.
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:
S is scoped to the inner function returned by the HOF and cannot be dynamically bound from the context where MapArray applies the functionWhat you need to do:
S and return type can be bound from MapArrayMapArrayMapArray 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).
/* _____________ 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}${Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
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']>.
infer does not rescue the originalThe 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.
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.
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.
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.
mapArray(['HOF'], toLowerCase()) on a plain string[]: no as const, so the element type is string and the whole chain stays string[] through the widening guard.toLowerCase()('TS'), repeat(19)(...) and concat('!', '?')(...) called outside mapArray: these never touch Apply, which is why the generic call signatures have to survive the fix.repeat(19) on a two-character string builds a 38-character literal, so the repeat helper has to handle a multi-digit count, not a chain of 19 recursions.concat('.', '?') passes two arguments: A is inferred as the tuple ['.', '?'] and Concatenated walks it left to right.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges