Learn Advanced Types of TypeScript with type-challenges
Shoki Ishii
Posted on November 22, 2021
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>>
]
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>>
]
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
}
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[] }
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;
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
}
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
}
}
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
}
}
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], {}]>
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], {}]>
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>>
]
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>>
]
The short explanation of this is:
- Get each value from an array by using
T[number]
- Because
T[number]
extendsnever
, the default isnever
- Check if there is no value in the array by conditional type
- If no value (=left side),
never
is returned - 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'>,
]
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'>,
]
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'>
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']>>,
]
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]>>,
]
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.
Posted on November 22, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.