Learn Advanced Types of TypeScript with type-challenges

shoki

Shoki Ishii

Posted on November 22, 2021

Learn Advanced Types of TypeScript with type-challenges

One distinctive difference between TypeScript and JavaScript is the difference in typing: JavaScript is dynamically typed, where the data type is determined at runtime, while TypeScript is statically typed, where the type is defined in advance.

While being able to type makes it easier to notice errors at compile time, there are many times when typing does not work. So, let's practice typing!

What I use through this article is open source of type-challenges by Anthony Fu

No.0 Hello World (example)

✳️Problem

/* _____________ Your Code Here _____________ */
type HelloWorld = any // expected to be a string

/* _____________ Test Cases _____________ */
import { Equal, Expect, NotAny } from '@type-challenges/utils'

type cases = [
  Expect<NotAny<HelloWorld>>,
  Expect<Equal<HelloWorld, string>>
]
Enter fullscreen mode Exit fullscreen mode

In this case, you need to change this part type HelloWorld = any and remove the error.

✅My Solution

/* _____________ Your Code Here _____________ */
type HelloWorld = string // expected to be a string

/* _____________ Test Cases _____________ */
import { Equal, Expect, NotAny } from '@type-challenges/utils'

type cases = [
  Expect<NotAny<HelloWorld>>,
  Expect<Equal<HelloWorld, string>>
]
Enter fullscreen mode Exit fullscreen mode

This is an easy one and you simply may change any to string

So now, let's start with easy level problems.

No.1 Pick

✳️Problem

/* _____________ Your Code Here _____________ */

type MyPick<T, K> = any


/* _____________ Test Cases _____________ */
import { 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
}
Enter fullscreen mode Exit fullscreen mode

To solve this problem, you need to use Mapped types and Lookup types.

Mapped types

The basic form of Mapped Types is { [P in K]: T }. P is an identifier that can be used in T. K should be a type that can be assigned to string, and T is some type that can use P as a type parameter.
It is a little bit hard to understand, so let's think about the word map.
As map is literally used in TypeScript like array.map(), the type is also mapped like below.
K = 'title' | 'completed' | 'invalid'
K.map(k => return {k : T})
Other examples are below:

type Item = { a: string, b: number, c: boolean };

type T1 = { [P in "x" | "y"]: number };  // { x: number, y: number }
type T2 = { [P in "x" | "y"]: P };  // { x: "x", y: "y" }
type T3 = { [P in "a" | "b"]: Item[P] };  // { a: string, b: number }
type T4 = { [P in keyof Item]: Date };  // { a: Date, b: Date, c: Date }
type T5 = { [P in keyof Item]: Item[P] };  // { a: string, b: number, c: boolean }
type T6 = { readonly [P in keyof Item]: Item[P] };  // { readonly a: string, readonly b: number, readonly c: boolean }
type T7 = { [P in keyof Item]: Array<Item[P]> };  // { a: string[], b: number[], c: boolean[] }
Enter fullscreen mode Exit fullscreen mode

Here is more about Mapped types.

Lookup types

A lookup type, also called an indexed access type, allows access to the type of a given key. It is similar to the way you access the value of an object property, but you can access the type.

keyof

keyof T can describe means "the direct sum type of type T".

For example,

interface Person {
  name: string;
  age: number;
}

type PersonKey = keyof Person;
Enter fullscreen mode Exit fullscreen mode

In the example above, PersonKey can be a type 'name' | 'age'.
So in this problem, you can let K extend T, that is 'title' | 'description' | 'completed'.

Applying these features to this problem, my solution got following:

✅My Solution

/* _____________ Your Code Here _____________ */

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

/* _____________ Test Cases _____________ */
import { 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
}
Enter fullscreen mode Exit fullscreen mode

It may take some time to understand, but let's go to look at the next problem.

No.2 Readonly

✳️Problem

/* _____________ Your Code Here _____________ */

type MyReadonly<T> = any


/* _____________ Test Cases _____________ */
import { 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
  }
}
Enter fullscreen mode Exit fullscreen mode

This problem is also a bit similar to the previous one. The main difference is the position of typeof.

In MyPick<T, K extends keyof T>, the constraint type(=K) ({ [P in K]: T }) extends keyof T. But in this case, the constraint type(=K) itself is keyof T.

Depending on the definition (type variable) such as MyPick and MyReadonly,keyof is put in different place.

✅My Solution

/* _____________ Your Code Here _____________ */

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


/* _____________ Test Cases _____________ */
import { 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
  }
}
Enter fullscreen mode Exit fullscreen mode

No.3 Tuple to Object

✳️Problem

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


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

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

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y'}>>,
]

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
Enter fullscreen mode Exit fullscreen mode

You need to know you can get each value from an array by using T[number]. For example, from an array ['tesla', 'model 3', 'model X', 'model Y'], you can get each element by using T[number]. And then, with Mapped types, you can create an object like {[P in T[number]]:P}

✅My Solution

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


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

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

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y'}>>,
]

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
Enter fullscreen mode Exit fullscreen mode

No.4 First of Array

✳️Problem

/* _____________ Your Code Here _____________ */

type First<T extends any[]> = any

/* _____________ Test Cases _____________ */
import { 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>>
]
Enter fullscreen mode Exit fullscreen mode

In this problem, you can use T[number] which I explained in the previous problem.
And to solve this problem, conditional types are necessary for the handling of an empty array.

✅My Solution

/* _____________ Your Code Here _____________ */

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


/* _____________ Test Cases _____________ */
import { 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>>
]
Enter fullscreen mode Exit fullscreen mode

The short explanation of this is:

  1. Get each value from an array by using T[number]
  2. Because T[number] extends never, the default is never
  3. Check if there is no value in the array by conditional type
  4. If no value (=left side), never is returned
  5. Else, the first of array is returned

No.5 Length of Tuple

✳️Problem

/* _____________ Your Code Here _____________ */

type Length<T extends any> = any

/* _____________ Test Cases _____________ */
import { 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'>,
]
Enter fullscreen mode Exit fullscreen mode

The point in this problem is that you need to use readonly. As you can see from as const in the case data, the passed value is a tuple type, which you cannot make any change on it.
Since it is a tuple type, you need to add readonly to guarantee that the length of the array will not change.

Also, for T["length"], it is referring to the length property of the tuple type.

✅My Solution

/* _____________ Your Code Here _____________ */

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

/* _____________ Test Cases _____________ */
import { 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'>,
]
Enter fullscreen mode Exit fullscreen mode

More

Now, you can solve the problems with the knowledge introduced so far.

I will quickly explain other problems that can be solved with the knowledge

No.6 If

✳️Problem & ✅My Solution

/* _____________ Your Code Here _____________ */
// type If<C, T, F> = any //original code
type If<C, T, F> = C extends true?T:F


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<If<true, 'a', 'b'>, 'a'>>,
  Expect<Equal<If<false, 'a', 2>, 2>>,
]

// @ts-expect-error
type error = If<null, 'a', 'b'>
Enter fullscreen mode Exit fullscreen mode

First, make C extend true, then simply write the conditional type.

No.7 Concat

✳️Problem & ✅My Solution

/* _____________ Your Code Here _____________ */
// type Concat<T, U> = any //original code
type Concat<T extends any[], U extends any[]> = [...T, ...U]


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Concat<[], []>, []>>,
  Expect<Equal<Concat<[], [1]>, [1]>>,
  Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>,
  Expect<Equal<Concat<['1', 2, '3'], [false, boolean, '4']>, ['1', 2, '3', false, boolean, '4']>>,
]
Enter fullscreen mode Exit fullscreen mode

This solution is using a spread operator, and concat the two arrays.
For the type definition, because both arrays can have any type, they will extend any[].

No. 8 Push

✳️Problem & ✅My Solution

/* _____________ Your Code Here _____________ */
// type Push<T, U> = any // original code
type Push<T extends any[], U> = [...T, U]

/* _____________ Test Cases _____________ */
import { Equal, Expect, ExpectFalse, NotEqual } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Push<[], 1>, [1]>>,
  Expect<Equal<Push<[1, 2], '3'>, [1, 2, '3']>>,
  Expect<Equal<Push<['1', 2, '3'], boolean>, ['1', 2, '3', boolean]>>,
]
Enter fullscreen mode Exit fullscreen mode

First, let T extend any[] because the array can have any type of value. Then, by using a spread operator, you can connect T and U like [...T, U].

Summary

You can continue to see the other problems here.

In addition to mapped types and lookup types, there are a variety of other type specifications, so please check them out as you try.

💖 💪 🙅 🚩
shoki
Shoki Ishii

Posted on November 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

How Changsets reads config.json internally
javascript How Changsets reads config.json internally

November 22, 2024

Release 0.3 #1
typescript Release 0.3 #1

November 19, 2024

arrayToDict function in tRPC source code
javascript arrayToDict function in tRPC source code

November 2, 2024