iTranslated by AI
Solving TypeScript type challenges
Let's Solve TypeScript Type Challenges
Nakano as a Service
This article is a direct copy of the Markdown slides written with Slidev below.
SpeakerDeck - Let's Solve TypeScript Type Challenges
Motivation
- Our products are unified with TypeScript from the frontend to the backend
- To improve code quality, it might be beneficial to know TypeScript deeply
- While studying documentation theoretically is good, looking through it while solving problems is more practical
→ Let's do TypeScript type challenges
What is TypeScript Type Challenges?
- A collection of problems where you implement utility types that assist with type definitions in TypeScript
- Knowing utility types themselves leads to knowing what is possible in TypeScript
- Furthermore, by implementing them yourself, you can gain a deeper understanding of TypeScript's language features
The problems are published in the following GitHub repository:
GitHub - TypeScript type challenges
Outline
- The origin of TypeScript
- Basics of TypeScript types
- Solving Beginner's TypeScript type challenges
- Explanation of utility types
- Solution explanation
The Origin of TypeScript
JavaScript has no types
function add(a, b) {
return a + b;
}
add(5, "10"); // Output: "510"
add(5, [1, 2]); // Output: "51,2"
add(5, null); // Output: 5
add(5, undefined); // Output: NaN
- Causes unexpected behavior
- Cannot know if it works correctly until execution
- A hotbed for bugs
The Origin of TypeScript
Allowing types while keeping JavaScript syntax
function add(a: number, b: number): number {
return a + b;
}
add(5, "10"); // Error: Argument of type '"10"' is not assignable to parameter of type 'number'.
add(5, [1, 2]); // Error: Argument of type 'number[]' is not assignable to parameter of type 'number'.
add(5, null); // Error: Argument of type 'null' is not assignable to parameter of type 'number'.
add(5, undefined); // Error: Argument of type 'undefined' is not assignable to parameter of type 'number'.
- Checks types at compile time and removes the type parts (called transpilation)
function add(a, b) {
return a + b;
}
- Can add types to existing JS code assets → It has that much flexibility
Entrance to Flexibility: Generic Types
What are Generic Types: Functions that take types as arguments and return types
Example: Wanting to declare an object type that contains a numeric type (Without Generic Types)
type NumberBox = {
content: number
}
// Usage example
const box: NumberBox = {
content: 1
}
Entrance to Flexibility: Generic Types
Example: Also wanting to prepare versions for string, date, and boolean types (Without Generic Types)
type NumberBox = {
content: number
}
type StringBox = {
content: string
}
type DateBox = {
content: Date
}
type BooleanBox = {
content: boolean
}
...
Doing this is a bit tedious → Use Generic Types
Entrance to Flexibility: Generic Types
type Box<T> = {
content: T
}
type NumberBox = Box<number>
type StringBox = Box<string>
type DateBox = Box<Date>
type BooleanBox = Box<boolean>
// Usage example
const bbox: BooleanBox = { content: true }
const bbox: Box<boolean> = { content: true } // Can also be written directly
This <...> part is called a type argument.
- Type arguments of generic types → like arguments in functions
-
Generic types are the type version of functions
- Functions that take types as arguments and return types
Adding Type Constraints to Generic Type Arguments
Recap: Typing Function Arguments
function add(a: number, b: number): number {
return a + b;
}
I want to do the same for generic types.
Type Constraints with extends
type Box<T extends number> = {
content: T
}
type NumberBox = Box<number>
type StringBox = Box<string> // Type error
type DateBox = Box<Date> // Type error
type BooleanBox = Box<boolean> // Type error
Now, only number can be passed into T of Box<T>.
→ —The body craves strong typing—
Literal Types and Union Types
type Three = 3 // A type that only allows 3 (Literal Type)
type Four = 4 // A type that only allows 4 (Literal Type)
type ThreeOrFour = Three | Four // A type that allows only 3 or 4 (Union Type)
const x: ThreeOrFour = 3
const y: ThreeOrFour = 4
const z: ThreeOrFour = 334 // Type error
Using this, we can create a Box type that only accepts 3 or "four".
type ThreeOrFourBox<T extends 3 | "four"> = {
content: T
}
type FourBox = ThreeOrFourBox<"four">
type FourBox = ThreeOrFourBox<5> // Type error
const x: FourBox = { content: "four" }
const x: FourBox = { content: 4 } // Type error
Summary So Far
- TypeScript is a language that adds types to JavaScript.
- Generic types
<...>are functions that take types as arguments and return types. - Just like adding types to function arguments, we added type constraints to generic type arguments.
- We created types that only allow specific values like
3or"foo"using literal types. - We created types that allow
3OR"foo"using union types|. - All that's left is practice.
Let's solve TypeScript type challenges!
Note: Problem statements have been partially modified for simplicity.
Beginner 1-Pick: Problem
Implement the built-in type utility Pick<T, K> without using it, creating a type that extracts properties from T that are in K.
For example:
type Todo = {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
Beginner 1-Pick: Explanation
First, let's consider the constraints on the type arguments.
type MyPick<T extends object, K extends keyof T> = { /* To be written later */ }
T extends object
A type constraint ensuring that the type argument T is an object type and not a string, number, etc.
keyof T: keyof type operator
type Book = {
title: string;
price: number;
rating: number;
};
type BookKey = keyof Book;
// The above is equivalent to the following:
type BookKey = "title" | "price" | "rating";
Beginner 1-Pick: Explanation
Next, let's consider using K as the keys for the object type.
type MyPick<T extends object, K extends keyof T> = {
[X in K]: X /* T is not used yet */
}
{ [X in K]: X }: Mapped Types
It assigns each type from the union type given to the type variable K into the type variable X, similar to a for loop, and returns an object type with X as the keys. Also, X can be used on the right side of the : corresponding to that key X.
type AB = {
[X in "a" | "b"]: X
}
// This is equivalent to:
type AB = {
a: "a",
b: "b"
}
Beginner 1-Pick: Explanation
The completed version:
type MyPick<T extends object, K extends keyof T> = {
[X in K]: T[X]
}
T[X]: Indexed Access Types
When accessing an object type or array type using an index of type X, it returns the type of the property or element that can be returned.
type Todo = { // Example of an object type
title: string
completed: boolean
}
type TitleOfTodo = Todo["title"] // becomes string
type TitleOfTodo = Todo[string] // becomes string | boolean
type Arr = boolean[] // Example of an array type
type TypeOfArr = Arr[334] // becomes boolean
type TypeOfArr = Arr[number] // also becomes boolean
Beginner 1-Pick: Explanation
The completed version:
type MyPick<T extends object, K extends keyof T> = {
[X in K]: T[X]
}
Example: When the type {a:number, b:string, c:boolean} is assigned to T and the type "a" | "c" is assigned to K:
// Assigned only T
MyPick<{ a: number, b: string, c: boolean }, K extends "a" | "b" | "c">
// Also assigned K
MyPick<{ a: number, b: string, c: boolean }, "a" | "c">
=> {
[X in "a" | "c"]: { a: number, b: string, c: boolean }[X]
}
=> {
"a": { a: number, b: string, c: boolean }["a"],
"c": { a: number, b: string, c: boolean }["c"]
}
=> { a: number, c: boolean }
layout: center
By the way, you can start solving these problems from the following link without any environment setup!
GitHub - TypeScript type challenges
Beginner 2-Readonly: Problem
Implement a type that makes all properties of T read-only without using the built-in type utility Readonly<T>. Properties of the implemented type cannot be reassigned.
For example:
type 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
Beginner 2-Readonly: Explanation
The completed version:
type MyReadonly<T extends object> = {
readonly [X in keyof T]: T[X]
}
- You can add the
readonlyattribute to Mapped Types.
Example: Intermediate steps of type computation when the type { x: number, y: string } is given to T
MyReadonly<{ x : number, y: string }>
=> {
readonly [X in "x" | "y"]: { x: number, y: string }[X]
}
=> {
readonly x: { x: number, y: string }["x"],
readonly y: { x: number, y: string }["y"]
}
=> {
readonly x: number,
readonly y: string
}
Beginner 3-Tuple to Object: Problem
Implement a type that takes a tuple and converts it into an object type where each value of the tuple is used as both the key and the value.
For example:
type Tuple = ['tesla', 'model 3', 'model X', 'model Y']
type Result = TupleToObject<Tuple>
const r: Result = {
'tesla': 'tesla',
'model 3': 'model 3',
'model X': 'model X',
'model Y': 'model Y'
}
Beginner 3-Tuple to Object: Explanation
What is a tuple type anyway?
-
An array type where the number of elements and the type of each element are fixed.
-
Examples of simple array types:
number[],string[],"a"[],1[] -
Examples of tuple types:
[number, string],["a", 1, true] -
A stricter version of an array type:
[number, string] extends any[] -
When you want to assert that an array in literal notation is a tuple type, you write
[...] as const.
type T = ['tesla', 'model 3', 'model X', 'model Y'] // Declaration of a tuple type
const teslaArr: string[] = ['tesla', 'model 3', 'model X', 'model Y']
const teslaTuple: T = ['tesla', 'model 3', 'model X', 'model Y'] // This is an error (array in literal notation)
const teslaTuple: T = ['tesla', 'model 3', 'model X', 'model Y'] as const // Asserting that it is a tuple
Beginner 3-Tuple to Object: Explanation
Completed version
type TupleToObject<T extends any[]> = {
[X in T[number]]: X
}
T[number]: Indexed Access Types for tuple T
- Since tuple types, unlike array types, have defined types for each element, it returns those types.
- Example: If T is
["a", 1, true]→T[2]is typetrue, andT[number]is the union type"a" | 1 | true. - Example: If T is
string[]→ bothT[100]andT[number]are typestring.
TupleToObject<["a", "b", "c"]> = {
[X in ["a", "b", "c"][number]]: X
}
=> {
[X in "a" | "b" | "c"]: X
}
=> { a: "a", b: "b", c: "c" }
Beginner 4-First of Array: Problem
Implement First<T> that takes an array T and returns the type of its first property.
For example:
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // becomes 'a'
type head2 = First<arr2> // becomes 3
type head3 = First<[]> // becomes never
Beginner 4-First of Array: Explanation
Close but not quite:
type First<T extends any[]> = T[0]
We should be able to return the type at index 0 of the array (or tuple) using Indexed Access Types.
type head3 = First<[]> // We want this to be never, but it becomes undefined!
In this case, a never type is required, so we want to return the never type.
By the way, if you actually access an out-of-bounds index:
const emptyArr: [] = [] as const
console.log(emptyArr[0]) // undefined is output
Beginner 4-First of Array: Explanation
Completed version:
type First<T extends any[]> = T extends [] ? never : T[0]
T extends U ? X : Y: Conditional Types
- A type-level conditional operator that returns
Xwhen the relationshipT extends Uis satisfied, andYotherwise. - When
extendsis used outside of type arguments<...>, it is almost always Conditional Types.
type R1 = 1 extends number ? "YES!" : "NO!" // becomes "YES!" type
type R2 = "x" extends boolean ? true : false // becomes false type
// When [] is passed to T in T extends [] ? never : T[0]
type R3 = [] extends [] ? never : [][0] // becomes never type
Beginner 5-Length of Tuple: Problem
Implement a type Length<T> that takes a tuple T and returns its length.
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
Beginner 5-Length of Tuple: Explanation
Completed version:
type Length<T extends any[]> = T["length"]
T["length"]: Indexed Access Types for Properties
In JS, the length property of an array object returns the number of elements in the array. Also, index access is equivalent to accessing via ..
const arr = [1, 2, 3]
arr.length // becomes 3
arr["length"] // index access and "." access are equivalent
And since tuple types have a fixed length, the type of the length property for a tuple type becomes a numeric literal type.
Beginner 6-Exclude: Problem
Implement a type that excludes from T those types that are assignable to U, without using the built-in type utility Exclude <T, U>.
For example:
type Result = MyExclude<1 | 2 | 3 | 4, 1 | 3> // becomes 2 | 4
Beginner 6-Exclude: Explanation
Completed version
type MyExclude<T, U> = T extends U ? never : T
Distributive Conditional Types
Example: When a union type 1 | 2 is passed to T in MyExclude<T, U>, it is distributed as follows:
(1 | 2) extends U ? never : T
=> (1 extends U ? never : 1) | (2 extends U ? never : 2)
This is the same as the distributive law of multiplication in mathematics:
More precisely, this occurs when a union type is passed to T in T extends U ? X : Y.
Beginner 6-Exclude: Explanation
Completed version
type MyExclude<T, U> = T extends U ? never : T
Additionally, when taking a union of the never type and other types, the never type has the property of being removed.
MyExclude<1 | 2 | 3, 2>
=> (1 extends 2 ? never : 1) | (2 extends 2 ? never : 2) | (3 extends 2 ? never : 3)
=> 1 | never | 3
=> 1 | 3
Beginner 7-Awaited: Problem
How can we retrieve the type wrapped within a Promise-like type?
For example: If there is a type like Promise<ExampleType>, how can we obtain ExampleType?
type R1 = MyAwaited<Promise<string>> // string
type R2 = MyAwaited<Promise<{ field: number }>> // { field: number }
type R3 = MyAwaited<Promise<Promise<string | number>>> // string | number
type R4 = MyAwaited<Promise<Promise<Promise<string | boolean>>>> // string | boolean
type Err = MyAwaited<string> // Error because it's not a Promise
Beginner 7-Awaited: Explanation
An almost-correct implementation:
type MyAwaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
infer U: Type Inference
- A keyword that can be used after
Yin Conditional Types (X extends Y ? L : R). - In
X extends Y<infer Z>, ifX extends Y<any>is satisfied, the type matching theanypart is assigned to the type variableZ, making it available in theLpart.
MyAwaited<Promise<string>>
=> Promise<string> extends Promise<infer U /* inferred as string */ > ? U : never
=> string
However, this alone cannot handle nested Promises like the following:
MyAwaited<Promise<Promise<string>>>
=> Promise<Promise<string>> extends Promise<infer U> ? U : never
=> Promise<string>
Beginner 7-Awaited: Explanation
The problem can be solved by checking if the inferred U is a Promise<any> and, if so, calling it recursively.
type MyAwaited<T extends Promise<any>>
= T extends Promise<infer U>
? U extends Promise<any>
? MyAwaited<U>
: U
: never;
Example
MyAwaited<Promise<Promise<string>>>
=> Promise<Promise<string>> extends Promise<infer U /* inferred as Promise<string> */ >
? Promise<string> extends Promise<any>
? MyAwaited<Promise<string>>
: Promise<string>
: never;
=> MyAwaited<Promise<string>>
=> Promise<string> extends Promise<infer U /* inferred as string */ >
? string extends Promise<any>
? MyAwaited<string>
: string
: never;
=> string
Beginner 8-If: Problem
Implement If, which takes a condition C, a return type T for when C is truthy, and a return type F for when C is falsy.
C is expected to be either true or false, while T and F can be any types.
For example:
type A = If<true, 'a', 'b'>; // expected to be 'a'
type B = If<false, 'a', 'b'>; // expected to be 'b'
Beginner 8-If: Explanation
Completed version
type If<C extends boolean, T, F> = C extends true ? T : F
It suddenly became very simple...
By the way, boolean is equivalent to true | false.
Beginner 9-Concat: Problem
Implement the JavaScript Array.concat function in the type system. This type takes two arguments and returns a new array containing the elements of the received iterators in order.
For example:
type Result = Concat<[1], [2]>; // expected to be [1, 2]
Beginner 9-Concat: Explanation
Completed version
A pure knowledge problem.
type Concat<T extends any[], U extends any[]> = [...T, ...U]
[...T]: Variadic Tuple Types
- When
Tis a tuple type, it returns a new tuple type withTexpanded into a part of it.
type T1 = ["a", "b", "c"]
type T2 = ["start", ...T1, "end"] // becomes ["start", "a", "b", "c", "end"]
Beginner 10-Includes: Problem
Implement the JavaScript Array.include function in the type system. This type takes two arguments and must output true or false.
For example:
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
However, you may use Equal<T, U>.
Equal<1, 1> // true
Equal<1, "a"> // false
Beginner 10-Includes: Explanation
Implementing as far as we can understand simply.
type Includes<T extends any[], U> = /* Think later */ ? true : false
Ideally, we want to extract elements from T one by one like a for loop, compare them with U, and return true if they match. However, since there is no for loop, we consider recursion.
- If
Tis[], returnfalse. - Compare the type of the first element of
TwithU.- If they are equal, return
true.
- If they are equal, return
- If they are not equal, prepare a tuple
Rthat removes the first element fromT. - Return
Includes<R, U>.
// Pseudo-code
includes([1, 2, 3], 2)
=> 1 != 2, so return includes([2, 3], 2)
=> 2 == 2, so return true
Beginner 10-Includes: Explanation
Implementing steps 1 and 2 of the recursive step partially.
Implemented up to the point of extracting the first element H and the remainder R from tuple T, and returning false if it is empty.
type Includes<T extends any[], U> = T extends [infer H, ...infer R]
? /* Think later */
: false;
T extends [infer H, ...infer R] ? X : Y
- A combination of Conditional Types, Variadic Tuple Types, and Type Inference (a new feature in TS 4.0).
- When
Tis a tuple type,His assigned the type of the first element ofT, andRis assigned the tuple type with the first element removed. - Example:
[1, 2, 3] extends [infer H, ...infer R]→His1,Ris[2, 3] - Example:
[1] extends [infer H, ...infer R]→His1,Ris[] - Example:
[] extends [infer H, ...infer R]→ It doesn't hold in the first place, soYis called.
Beginner 10-Includes: Explanation
Extracted the first element H and the rest R from tuple T.
type Includes<T extends any[], U> = T extends [infer H, ...infer R]
? /* Think later */
: false;
Completed version
All that's left is to compare H and U, and if they are equal return true, otherwise return Includes<R, U>.
type Includes<T extends any[], U>
= T extends [infer H, ...infer R]
? Equal<{H}, U> extends true
? true
: Includes<{R}, U>
: false;
Beginner 10-Includes: Explanation
Completed version
type Includes<T extends any[], U>
= T extends [infer H, ...infer R]
? Equal<{H}, U> extends true
? true
: Includes<{R}, U>
: false;
Example: When T is [1, 2, 3] and U is 2
Includes<[1, 2, 3], 2>
=> [1, 2, 3] extends [infer H /* 1 */, ...infer R /* [2, 3] */]
? Equal<1, 2> extends true
? true
: Includes<[2, 3], 2>
: false;
=> Includes<[2, 3], 2>
=> [2, 3] extends [infer H /* 2 */, ...infer R /* [3] */]
? Equal<2, 2> extends true
? true
: Includes<[3], 2>
: false;
=> true
Beginner 11-Push: Problem
Implement a generic version of Array.push.
For example:
type Result = Push<[1, 2], boolean> // [1, 2, boolean]
Beginner 11-Push: Explanation
Completed version
Solved in one shot with Variadic Tuple Types
type Push<T extends any[], U> = [...T, U]
Beginner 12-Unshift: Problem
Implement the type version of Array.unshift.
For example:
type Result = Unshift<[1, 2], 0> // [0, 1, 2]
Beginner 12-Unshift: Explanation
Completed version
With Variadic Tuple Types (...)
type Unshift<T extends any[], U> = [U, ...T]
Beginner 13-Parameters: Problem
Implement a type that constructs a tuple type from T without using the built-in type utility Parameters<T>.
For example:
type F = (arg1: string, arg2: number) => void // Function type
// Example usage of function type
function f(arg1: string, arg2: number) {
console.log("foo")
}
const f1: F = f
// Example usage of function type with an arrow function
const f: F = (arg1: string, arg2: number): void => {
console.log("foo")
}
type FunctionParamsType = MyParameters<F> // becomes [string, number]
Beginner 13-Parameters: Explanation
Completed version
infer can also be used for function types
type MyParameters<T extends (...args: any[]) => any>
= T extends (...args: infer U) => any
? U
: never;
Final Thoughts
- It was a continuous series of surprises, like "You can write this here!?"
- I was able to break free from my own preconceptions about types.
- I tried the Intermediate level up to the middle afterward, and it became fun writing recursive logic within those unique constraints.
- I highly recommend you try the rest yourself.
- Things not explained:
- The relationship between
neverandanytypes - The difference between
extendsandEqual<T, U> - Implementation of
Equal<T, U> - And so on...
- The relationship between
Discussion