Learn Advanced Types of TypeScript with type-challenges
Tags: typescript, opensource
Published: 2021-11-22 09:06:13

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](https://github.com/type-challenges/type-challenges) by [Anthony Fu](https://github.com/antfu)


## No.0 [Hello World (example)](https://github.com/type-challenges/type-challenges/blob/master/questions/13-warm-hello-world/README.md)
### ✳️Problem
```typescript
/* _____________ 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>>
]
```
In this case, you need to change this part `type HelloWorld = any` and remove the error. 

### ✅My Solution
```typescript
/* _____________ 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>>
]
```
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](https://github.com/type-challenges/type-challenges/blob/master/questions/4-easy-pick/README.md)
### ✳️Problem
```typescript
/* _____________ 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
}
```
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:
```typescript
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[] }
```
[Here](https://github.com/Microsoft/TypeScript/pull/12114) 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,
```typescript
interface Person {
  name: string;
  age: number;
}

type PersonKey = keyof Person;
```
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
```typescript
/* _____________ 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
}
```

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

## No.2 [Readonly](https://github.com/type-challenges/type-challenges/blob/master/questions/7-easy-readonly/README.md)
### ✳️Problem
```typescript
/* _____________ 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
  }
}
```
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
```typescript
/* _____________ 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
  }
}
```
## No.3 [Tuple to Object](https://github.com/type-challenges/type-challenges/blob/master/questions/11-easy-tuple-to-object/README.md)
### ✳️Problem
```typescript
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], {}]>
```
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
```typescript
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], {}]>
```
## No.4 [First of Array](https://github.com/type-challenges/type-challenges/blob/master/questions/14-easy-first/README.md)
### ✳️Problem
```typescript
/* _____________ 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>>
]
```
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
```typescript
/* _____________ 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>>
]
```
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](https://github.com/type-challenges/type-challenges/blob/master/questions/18-easy-tuple-length/README.md)
### ✳️Problem
```typescript
/* _____________ 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'>,
]
```
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
```typescript
/* _____________ 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'>,
]
```

## 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](https://github.com/type-challenges/type-challenges/blob/master/questions/268-easy-if/README.md)
### ✳️Problem &  ✅My Solution
```typescript
/* _____________ 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'>
```
First, make `C` extend `true`, then simply write the conditional type.

## No.7 [Concat](https://github.com/type-challenges/type-challenges/blob/master/questions/533-easy-concat/README.md)
### ✳️Problem &  ✅My Solution
```typescript
/* _____________ 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']>>,
]
```
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](https://github.com/type-challenges/type-challenges/blob/master/questions/3057-easy-push/README.md)
### ✳️Problem &  ✅My Solution
```typescript
/* _____________ 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]>>,
]
```
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](https://github.com/type-challenges/type-challenges).

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