Skip to content

Type Gymnastic - Part 4

Posted on:September 19, 2023 at 02:00 PM

This is part 4 of the series “Type Gymnastic”. If you haven’t already, go checkout part 3. 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 9.

Intermediate challenges

The last part of this series went through some basics of Typescript, and even some basics of computer science. The next challenges might be a bit more advanced, let’s go through them.

Challenge 9

You are given the type

type MyReturnType<T> = any;

and you want to construct a type that accepts a function as a generic and figure out its return type. An example would be:

type Example = MyReturnType<() => void>; // Example = void;

We can start by adding conditional types to express that the generic T is a function:

type MyReturnType<T> = T extends () => any ? "A function" : "Not a function";

Here we are saying if T extends a function with no parameters and a return type of any, set the type equal to "A function" else set the type equal to "Not a function". Now if we were to pass a function with parameters as the generic, this would return "Not a function":

type Example = MyReturnType<(a: string) => void>; // Example = "Not a function"

We want the function to accept any amount of arguments, and we can express that by using rest parameters:

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

Lastly, we want to extract the return type from (...args: any[]) => any. Luckily for us, with Typescript it is possible to have infer declarations together with conditional types:

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

infer allows us to define variables inside the conditional types and be used or returned later on. To finish this challenge we can add a constraint to the generic so we are not allowed to pass anything else other than functions, and let the falsy conditional be never:

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

Challenge 10

You are given the type

type NonEmptyPropertyType<T, K> = any;

and you want to construct a type which accepts a non-empty object, T, and picks a single value from it based on K. An example would be:

type Example1 = NonEmptyPropertyType<{ a: string }, "a">; // Example1 = string
type Example2 = PropertyType<{ one: string; two: number }, "one">; // Example2 = string

This challenge is similar to Challenge 4 in part 2, however there is a bit of a twist to this. We can start with the naïve approach first though:

type NonEmptyProperty<
  T extends Record<PropertyKey, unknown>,
  K extends keyof T,
> = T[K];

Here we are saying that T has to be a record with any valid property key (string, number or symbol) and the values are unknown. K has to be any key that exists on T. Looks good, right? Well, if we use it like this:

type Example = NonEmptyProperty<{}, never>;

This is still allowed, but we want our type to throw a type error if we pass an empty object to it. Which means we have to extend our constraints for T. We can try adding a conditional to see if T extends {}:

type NonEmptyProperty<T extends {} ? never : Record<PropertyKey, unknown>,
K extends keyof T> = T[K];

However, if you try this for yourself you will see that this syntax is not allowed inside generic constraints. Instead we can try something a bit different, a recursive constraint:

type NonEmptyProperty<
  T extends {} extends T ? never : Record<PropertyKey, unknown>,
  K extends keyof T,
> = T[K];

Here we are constraining T to a conditional type, {} extends T ? never : Record<PropertyKey, unknown>. If you can assign an empty object to T, then T should be never, if you cannot, then T should be Record<PropertyKey, unknown>. Which is the solution to this challenge!

Challenge 11

You are given the type

type MatrixColumn<T, I> = any;

and you want to construct a type which accepts a 2-dimensional matrix, T, and an index, I, and return column I on matrix T. If the index is invalid, return a column with never values. An example would be:

type Matrix1 = [[1, 2], [3, 4]];

type Column = MatrixColumn<Matrix1, 0>; // Column = [1,3]

For this challenge, we need to use mapped types and the keyof type operator. First we want to map over all keys of the matrix T:

type MatrixColumn<T, I> = {
  [P in keyof T]: any;
};

When using keyof on array all we do is retrieve the array’s indices. We can see how the keyof type operator works with some more examples:

// Type to map over all keys of T and set the type equal to the key
type A<T> = {
  [P in keyof T]: P;
};

type A1 = A<[1]>; // A1 = "0"
type A2 = A<[1, 2]>; // A2 = ["0", "1"]
type A3 = A<[[1, 2], [3, 4]]>; // A3 = ["0", "1"]

Next thing we want to do is to extract the column values from the 2-dimensional array. To retrieve the columns we have to index first on the matrix’ row and then column:

type MatrixColumn<T, I> = {
  [P in keyof T]: T[P][I];
};

If you try this yourself you will see that Typescript is not happy because it doesn’t know that I is a valid index on T[P]. A solution to this is to add a conditional type to see if I extends keyof T[P]:

type MatrixColumn<T, I> = {
  [P in keyof T]: I extends keyof T[P] ? T[P][I] : never;
};

One would think that this should be good enough, however if you use this type now i.e. MatrixColumn<[[1,2],[1,2]], 32> you would get [undefined, undefind]. Meaning I extends keyof T[P] or 32 extends keyof T[P] is truthy. It seems like Typescript will treat any number (both positive and negative) as a valid index on arrays. So we need to add another conditional to see if T[P][I] is undefined:

type MatrixColumn<T, I> = {
  [P in keyof T]: I extends keyof T[P]
    ? T[P][I] extends undefined
      ? never
      : T[P][I]
    : never;
};

Which is the solution to this challenge! Part 5 coming soon. Part 5

Relevant reading