Skip to content

Type Gymnastic - Part 5

Posted on:September 24, 2023 at 07:00 PM

This is part 5 of the series “Type Gymnastic”. If you haven’t already, go check out part 4. Let’s continue with more challenges today!

Table of contents

Open Table of contents

Intro

In this series of type challenges we do a walk through of all challenges that I used in my advent calender. We will continue from where we left off - challenge 12.

Intermediate ++ challenges

We are in the stage of this series where the challenges will get harder in difficulty, new concepts and tricks will get introduced. Let’s go through them now.

Challenge 12

You are given the type

type MyReturnType2<T> = any;

and you want to construct a type that accepts a function as a generic and figure out its return type. If the return type is a Promise you want to unwrap it so you get the value of the promise. An example would be:

type Example = MyReturnType2<(a: string) => void>; // Example = void
type Example2 = MyReturnType2<() => Promise<string>>; // Example = string
type Example3 = MyReturnType2<() => Promise<Promise<string>>>; // Example = string

As we can see from the examples above, there is a possibility that we need to unwrap promises X amount of times. But first, let’s figure out the the return type of a function without thinking about promises. This was actually a challenge in the previous part of this series. You can go take a look at it here if you haven’t already. In order to find the return type of a function that can accept any number of arguments we can first express the function type:

type MyReturnType2<T> = T extends (...args: any[]) => any
  ? "A function"
  : "Not a function";

And to retrieve the return type, we change the any type to infer R. The infer keyword let’s us extract the type and use it at a variable in the conditional type.

type MyReturnType2<T> = T extends (...args: any[]) => infer R ? R : never;

Now we have a type that is able to figure out the return type of a function. Next thing we want to do is to check if the return type is a Promise:

type MyReturnType2<T> = T extends (...args: any[]) => infer R
  ? R extends Promise<any>
    ? "Promise"
    : R
  : never;

If it’s a promise we return “Promise”, if it’s not a promise we return the inferred return type R. Instead of Promise<any> we can add another infer to extract the type of the promise:

type MyReturnType2<T> = T extends (...args: any[]) => infer R
  ? R extends Promise<infer P>
    ? P
    : R
  : never;

Now we are returning the promise type P if the return type of the function is a promise. However, it’s possible to have: Promise<Promise<string>>, so what we actually want to do with the inferred P type is to recursively use our type to unwrap all the promises:

type MyReturnType2<T> = T extends (...args: any[]) => infer R
  ? R extends Promise<infer P>
    ? MyReturnType<() => P>
    : R
  : never;

Which is the solution to this challenge!

Challenge 13

You are given the type

type CamelCase<S> = any;

and you want to construct a type that accepts a string and camelizes it if is written in camel-case. It’s expected that camelcase is written like camelCase and kebab-case is written like hello-world. However, we should also support inputs such as merry--christmas- and ho-ho-ho-ho Some examples of possible inputs and expected outputs:

type Example1 = CamelCase<"santa-claus">; // Example1 = "SantaClaus"
type Example2 = CamelCase<"ho-ho-ho-ho">; // Example2 = "hoHoHoHo"
type Example3 = CamelCase<"merry--christmas-">; // Example3 = "merryChristmas"

So there are a few things to go through in this challenge. Let’s start easy, the generic S has to be of type string:

type CamelCase<S extends string> = any;

Next thing we want to do is infer different parts of the string and use conditional types to see if the string contains ”-” somewhere.

With Typescript we can have a type like type World = "world" which is a string literal type. It is also possible to have a template literal type:

type World = "world";
type Hello = `hello ${World}`;

This follows the same syntax as a template literal in JavaScript:

const aVariable = "World";
const t = `Hello ${aVariable}`;

Coming back to the original challenge, with the knowledge of template literal types, inferred types and conditional types we can try to extract some information about the generic string S. Remember that we want to figure out if the string is written in kebab-case so we want to check if ”-” exists in the string.

type CamelCase<S extends string> = S extends `${infer Prefix}-${infer Suffix}`
  ? Prefix
  : never;

Remember that infer let’s us extract the type into a variable and use it for later. In the code above we infer the string content before the character ”-” and call it Prefix, and the same with the string content after the character ”-” and call it Suffix. If we were to use this type now we would get:

type A1 = CamelCase<"hello">; // A = never
type A2 = CamelCase<"hello-world">; // A2 = "hello"

And if we change the return type from Prefix to Suffix we would get:

type CamelCase<S extends string> = S extends `${infer Prefix}-${infer Suffix}`
  ? Suffix
  : never;
type A1 = CamelCase<"hello">; // A = never
type A2 = CamelCase<"hello-world">; // A2 = "world"

To only infer the prefix and suffix is not enough for this challenge. Since we want to camelize the string, we also need to change the first character after ”-” to be uppercase i.e. hello-world should be helloWorld. We can add another infer right before the suffix to extract the character after ”-“:

type CamelCase<S extends string> =
  S extends `${infer Prefix}-${infer R}${infer Suffix}` ? Prefix : never;

To be able to manipulate strings from lowercase to uppercase using only types might seem confusing, but luckily for us Typescript comes with some built-in utilities for exactly this:

These can be used like normal types, and you don’t have to do anything special to be able to use them. However, they are built-in in the Typescript compiler and are sometimes referred to “intrinsic”. They use the JavaScript string runtime function to manipulate the string:

function applyStringMapping(symbol: Symbol, str: string) {
  switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
    case IntrinsicTypeKind.Uppercase:
      return str.toUpperCase();
    case IntrinsicTypeKind.Lowercase:
      return str.toLowerCase();
    case IntrinsicTypeKind.Capitalize:
      return str.charAt(0).toUpperCase() + str.slice(1);
    case IntrinsicTypeKind.Uncapitalize:
      return str.charAt(0).toLowerCase() + str.slice(1);
  }
  return str;
}

For this challenge we can take use of the Uppercase type. Let’s try to add a return type that resembles a camelized string:

type CamelCase<S extends string> =
  S extends `${infer Prefix}-${infer R}${infer Suffix}`
    ? `${Prefix}${Uppercase<R>}${Suffix}`
    : S;

This solves some of our test cases like “hello-world”, however if there are more than one ”-”, we are not able to change it because this type is not recursive. So instead of returning ${Prefix}${Uppercase<R>}${Suffix}, let’s instead use CamelCase until there are no more ”-” in the string:

type CamelCase<S extends string> =
  S extends `${infer Prefix}-${infer R}${infer Suffix}`
    ? CamelCase<`${Prefix}${Uppercase<R>}${Suffix}`>
    : S;

Which solves the issue with multiple hyphens. But there is one more test case remaining that was added to make this a bit more challenging:

type A1 = CamelCase<"hello-world-">; // A1 = "helloWorld-"

So there is one last thing to check: If the string S is equal to some string content with a hyphen at the end of it.

type CamelCase<S extends string> =
  S extends `${infer Prefix}-${infer R}${infer Suffix}`
    ? CamelCase<`${Prefix}${Uppercase<R>}${Suffix}`>
    : S extends `${infer P}-`
      ? P
      : S;

Which is the solution to this challenge!

Challenge 14

You are given the type

type ReplaceAll<S extends string, From, To> = any;

and you want to construct a type which takes three generics: first one will be a string, the second a subset of the first generic string that is to be replaced by the last generic. An example would be:

type A = ReplaceAll<"hello world", "world", "you">; // A = "hello you"

First thing we can check is if the subset From generic exists in the original string S by using template literal types and inferrence

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string,
> = S extends `${infer L}${From}${infer R}` ? "yes" : "no";

And if we were to use it now with some example values:

type A1 = ReplaceAll<"hello world", "hello", "hey">; // A1 = "yes"
type A2 = ReplaceAll<"hello", "a", "b">; // A2 = "no"
type A3 = ReplaceAll<"hello", "he", "ho">; // A3 = "yes"
// Notice that A3 is "yes", even though it looks like the ReplaceAll type
// expects the `From` generic ("he" in this case) to have a suffix `infer L`.

Instead of returning “yes” and “no”, let’s replace the string content using the last generic To:

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string,
> = S extends `${infer L}${From}${infer R}` ? `${L}${To}${R}` : S;

Which solves a lot of the test cases for this challenge, however if the occurence of the word you want to replace is more than one we won’t be able to change it. So we have to be recursive:

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string,
> = S extends `${infer L}${From}${infer R}`
  ? `${L}${To}${ReplaceAll<R, From, To>}`
  : S;

Lastly, there are one more edge case if we pass empty strings to some of the generics. For example:

type A1 = ReplaceAll<"SantaClaus", "", "Klaus">;
//A1 = "SKlausaKlausnKlaustKlausaKlausCKlauslKlausaKlausuKlaussKlaus"

So let’s add a conditional type for the From generic to see if it’s an empty string:

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string,
> = From extends ""
  ? S
  : S extends `${infer L}${From}${infer R}`
    ? `${L}${To}${ReplaceAll<R, From, To>}`
    : S;

Which is the solution to this challenge! And there are also alternative solutions to many of these challenges. For example another solution could be:

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string,
> = S extends `${infer R}${From}${infer Suffix}`
  ? S extends `${R}${Suffix}`
    ? S
    : ReplaceAll<`${R}${To}${Suffix}`, From, To>
  : S;

Part 6 coming soon. Part 6

Relevant Reading