Typescript challenge practice series(1)
Aaron Zhang Author

logo

Prefix

I found a repo on Github about Typescript not long ago which provides a bunch of exercises which helps us to understand the fundamental of Typescript. As most of projects nowadays are using Typescript to implement static typing, I decided to follow along and see how I’ll go with those challenges.

How to play around with it?

There are questions of different levels in the questions directory, each of which has a README, test-cases and template. The challenge is to modify the type in template to make test cases all pass in test-cases .

Let’s get started from the easy ones!

00004 Easy Pick

https://github.com/type-challenges/type-challenges/tree/main/questions/00004-easy-pick

Implement the built-in Pick<T, K> generic without using it.

Constructs a type by picking the set of properties K from T

For example:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = MyPick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

Tests

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<Expected1, MyPick<Todo, "title">>>,
  Expect<Equal<Expected2, MyPick<Todo, "title" | "completed">>>,
  // @ts-expect-error
  MyPick<Todo, "title" | "completed" | "invalid">
];

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

interface Expected1 {
  title: string;
}

interface Expected2 {
  title: string;
  completed: boolean;
}

Solution

type MyPick should work as the same as the native Pick type in Typescript, which will generated a new type the the picked property keys from the given type/interface with same value type of the picked key. See Pick in Typescript docs here: https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys

First of all, we need to ensure the second generic type will extend T‘s properties:

type MyPick<T, KEYS extends keyof T> = {};

Then, we can use mapped types to specify all key types of KEYS, and use indexed access type to specify the value type:

type MyPick<T, KEYS extends keyof T> = {
  [K in KEYS]: T[K];
};

At this stage, all test cases should pass with no complaining errors.

00007 Easy Readonly

https://github.com/type-challenges/type-challenges/tree/main/questions/00007-easy-readonly

Implement the built-in Readonly<T> generic without using it.

Constructs a type with all properties of T set to readonly, meaning the properties of the constructed type cannot be reassigned.

For example:

interface Todo {
  title: string;
  description: string;
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar",
};

todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property

Tests

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>];

interface Todo1 {
  title: string;
  description: string;
  completed: boolean;
  meta: {
    author: string;
  };
}

Solution

This is a simple one. The solution is to use mapped type to iterate through Todo and add readonly to each of its keys.

type MyReadonly<T> = {
  readonly [KEY in keyof T]: T[KEY];
};

This shall make its tests all pass.

00011 Easy Tuple

https://github.com/type-challenges/type-challenges/tree/main/questions/00011-easy-tuple-to-object

Give an array, transform into an object type and the key/value must in the given array.

For example:

const tuple = ["tesla", "model 3", "model X", "model Y"] as const;

type result = TupleToObject<typeof tuple>; // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

Tests

import type { Equal, Expect } from "@type-challenges/utils";

const tuple = ["tesla", "model 3", "model X", "model Y"] as const;
const tupleNumber = [1, 2, 3, 4] as const;
const tupleMix = [1, "2", 3, "4"] as const;

type cases = [
  Expect<
    Equal<
      TupleToObject<typeof tuple>,
      {
        tesla: "tesla";
        "model 3": "model 3";
        "model X": "model X";
        "model Y": "model Y";
      }
    >
  >,
  Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1; 2: 2; 3: 3; 4: 4 }>>,
  Expect<
    Equal<TupleToObject<typeof tupleMix>, { 1: 1; "2": "2"; 3: 3; "4": "4" }>
  >
];

// @ts-expect-errorr
type error = TupleToObject<[[1, 2], {}]>;

Solution

First we need to ensure that this type accepts const arrays(tuples):

type TupleToObject<T extends readonly any[]> = any;

Next, we want to get all of the value inside of it as a union type to iterate through the tuple just like we use mapped type to iterate through an object type.
In array/tuple, we cannot use keypf T operator, instead we can treat array as an object-like type that has number as key of each of its element - we can do T[number] to get all of its values. So it’s going to be like this:

type TupleToObject<T extends readonly any[]> = {
  [VAL in T[number]]: VAL;
};

Till now we solve most of the test cases, except for the error handling case:

type error = TupleToObject<[[1, 2], {}]>;

We can see it doesn’t like empty object or array/tuple element to be inside. So the simplest way is to use extends to constrain the type to be number or string

type TupleToObject<T extends readonly (number | string)[]> = {
  [VAL in T[number]]: VAL;
};

Now it’s all good!

00014 Easy First of Array

https://github.com/type-challenges/type-challenges/tree/main/questions/00014-easy-first

Implement a generic First<T> that takes an Array T and returns it’s first element’s type.
For example:

type arr1 = ["a", "b", "c"];
type arr2 = [3, 2, 1];

type head1 = First<arr1>; // expected to be 'a'
type head2 = First<arr2>; // expected to be 3

Tests

import type { Equal, Expect } from "@type-challenges/utils";

type cases = [
  Expect<Equal<First<[3, 2, 1]>, 3>>,
  Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
  Expect<Equal<First<[]>, never>>,
  Expect<Equal<First<[undefined]>, undefined>>
];

type errors = [
  // @ts-expect-error
  First<"notArray">,
  // @ts-expect-error
  First<{ 0: "arrayLike" }>
];

Solution 1

Our first thought was like this:

type First<T extends any[]> = T[0];

This will solve most of test cases, but failed when T is an empty array. So we can add a conditional return:

type First<T extends any[]> = T extends [] ? never : T[0];

Solution 2

Instead of checking T extends [], we can check its length type. From previous tests we know there’s a length property on array/tuple type, so we can do:

type First<T extends any[]> = T["length"] extends 0 ? never : T[0];

Solution 3

We can also check if T[0] is in array:

type First<T extends any[]> = T[0] extends T[number] ? T[0] : never;

Solution 4

We can also use infer to get the type of the first element of the array:

type First<T extends any[]> = T extends [infer FIRST, ...infer REST]
  ? FIRST
  : never;

00018 Easy Tuple Length

https://github.com/type-challenges/type-challenges/tree/main/questions/00018-easy-tuple-length

Create a generic Length, pick the length of the tuple.

For example:

type tesla = ["tesla", "model 3", "model X", "model Y"];
type spaceX = [
  "FALCON 9",
  "FALCON HEAVY",
  "DRAGON",
  "STARSHIP",
  "HUMAN SPACEFLIGHT"
];

type teslaLength = Length<tesla>; // expected 4
type spaceXLength = Length<spaceX>; // expected 5

Tests

import type { Equal, Expect } from "@type-challenges/utils";

const tesla = ["tesla", "model 3", "model X", "model Y"] as const;
const spaceX = [
  "FALCON 9",
  "FALCON HEAVY",
  "DRAGON",
  "STARSHIP",
  "HUMAN SPACEFLIGHT",
] as const;

type cases = [
  Expect<Equal<Length<typeof tesla>, 4>>,
  Expect<Equal<Length<typeof spaceX>, 5>>,
  // @ts-expect-error
  Length<5>,
  // @ts-expect-error
  Length<"hello world">
];

Solution

This is an easy one. From the examples and tests we can see Length accepts constant tuple. and we can use length property of array/tuple type to get the length. So it’ll be like this:

type Length<T extends readonly any[]> = T["length"];

Summary

👉 keyof T Operator

To get all keys as a union type from a type or interface, we can use mapped types in.

type Point = { x: number; y: number };
type P = keyof Point;

// P is the same type as “x” | “y”

More on this: https://www.typescriptlang.org/docs/handbook/2/keyof-types.html#handbook-content

👉 [KEY in KTYPES]

A mapped type is a generic type which uses a union of PropertyKeys (frequently created via a keyof) to iterate through keys to create a type.

More on this:
https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content

👉 TODO[KEY]

Indexed access type. We can use an indexed access type to look up a specific property on another type:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];

// type Age = number

Indexed access type can also get the length of an array as a type.

const arr = [1, 3, 4];
type TLength = arr["length"];

More on this: https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html#handbook-content

👉 extends

The keyword extends stands for constraints when defining a generic type.

👉 Native ReadOnly type will do as the following:

 interface Todo {
   title: string
   description: string
 }

 const todo: Readonly<Todo> = {
   title: "Hey",
   description: "foobar"
 }

 // will make `todo` as a readonly object:
  const todo: <Todo> = {
   readonly title: "Hey",
   readonly description: "foobar"
 }

More on ReadOnly: https://www.tutorialsteacher.com/typescript/typescript-readonly

👉 as const operator

const tuple = ['tesla', 'model 3'] as const

// will be equivalent to

typeof tuple = readonly ['tesla', 'model 3']`

👉 To get all values from an array type:

We can do ARR[number] to get all values as an union type of the array type ARR and use E in ARR[number] to iterate through.

👉 To get type of the first element in an array

T[0]

👉 Get length of an array

T['length'] //also known as indexed

👉 extends union type

If we check whether a extends a|2|3, it will check every type in union type to see if they match.

👉 infer in array destruction

T extends [infer FIRST, ...infer REST] ? FIRST : never

if FIRST can be successfully destructed, then return FIRST

const arr: any[] = []
const [a, ...b] = arr

a ==> undefined
b ==> []

To be continued…

 Comments