TS Challenge Record (1)#
The problem is recorded here: https://github.com/type-challenges/type-challenges/blob/main/README.md
This challenge is mainly to help challengers better understand the TS type system, implementing various features in TS through the type
syntax, which feels quite interesting!
Is type
also a Turing complete system? That is, can it achieve metaprogramming similar to C++
templates? 🤔 It would be good to study this in depth when there's time: https://github.com/microsoft/TypeScript/issues/14833
Hello World Challenge:
// expected to be string
type HelloWorld = any
// you should make this work
type test = Expect<Equal<HelloWorld, string>>
The first example mainly demonstrates the usage of type
, with the answer:
type HelloWorld = string
Since the essence of this question is to help you understand the usage of type, let's first record some notes on learning type, otherwise, the later questions will really be hard to understand.
Main Uses of type#
Here is a summary of the main uses of type to prevent confusion in the following code (actually, I just did the questions first and then learned the type keyword in reverse 😅).
1. Basic Type Alias#
type Word = string;
This is easy to understand; it defines a custom type Word
to replace string
.
2. Union Types#
type StringOrNumber = string | number;
This starts to get a bit complex, but can be understood as a union
. Through StringOrNumber
, you can declare variables of type string or number.
Note that the types here can also be literal types, such as:
type ABC = "A" | "B" | "C"
This indicates that the variable of type ABC can only have the values "A", "B", "C"
.
3. Intersection Types#
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
To use this intersection type, you would do:
let person: Person = {
name: 'Alice',
age: 30
};
4. typeof && keyof#
typeof
is used to get the type of a variable or value, somewhat similar to C++
's type, but with broader applications.
For example, using the typeof
operator on a function can yield the corresponding type of the function, similar to C++
's auto type deduction.
const foo = (arg1: string, arg2: number): void => {}
You can define parameter types using typeof
:
const bar = (f: typeof foo) => {
f("Hello", 42);
}
keyof
is used to get all key names of a type, as shown in the example below:
type Person = {
name: string;
age: number;
}
type K = keyof Person; // "name" | "age"
extends#
extends
is mainly used to determine whether a given generic variable can be assigned to a specified type.
T extends U ? X : Y
Of course, you will find that
extends
has many magical uses later on 🤣.
in#
The in
operator in TS, besides its common usage, can also be used to iterate over types, serving as metaprogramming. For example:
type Keys = 'a' | 'b'
type MyMap<T> = {
[K in Keys]: T
}
Keys
represents the literal types that can be either 'a'
or 'b'
.
K in Keys
can iterate over these two types.
The final MyMap
structure can be understood as:
{
a: T, // 'a' type, actually the key must be a
b: T // 'b' type, actually the key must be b
}
infer#
The role of infer
is to specify the inferred type in a certain context (that's how I understand it).
Let's look at a simple example:
Here we use (...args: any) => infer R
, which is a function form. Imagine infer R
as a whole, representing the return value of the arrow function, so infer R
naturally equals the return value type.
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
Usage example: you can see that ReturnType
infers R
as string
.
type T = (a: number) => string
type R = ReturnType<T> // string
After supplementing this knowledge, you can start solving problems now 😉.
Implement MyPick#
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
By using keyof
, you can obtain a literal union type composed of all properties of the type, and then iterate using in
.
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
Implement MyReadonly#
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
Add readonly
to the properties.
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
}
Tuple to Object#
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'}
This idea isn't difficult, but I didn't think it through completely, so I looked at the answer directly:
type TupleToObject<T extends readonly (keyof any)[]> = {
[K in T[number]]: K;
};
Some key parts need explanation:
keyof any
actually represents all expressible enum types, i.e.,string | number | symbol
, because theany
type is these three 🤣.readonly (keyof any)[]
represents all read-only(string | number | symbol)[]
, i.e., a mixed-type array.[K in T[number]]
usesnumber
to represent numeric literals.
Why use
readonly (keyof any)[]
instead ofreadonly any[]
?
Because our result is an Object, and in JS, the keys of an Object can only be string
, number
, and symbol
. If we change it to T extends readonly any[]
, it could lead to values like undefined, boolean, null
becoming keys, causing errors.
Another part that is a bit difficult to understand is T[number]
. This can be understood with an example:
type Person = {
name: string;
age: number;
};
type PersonName = Person["name"]; // PersonName will be of type "string"
type PersonAge = Person["age"]; // PersonAge will be of type "number"
This can be understood as obtaining the corresponding value type through the T[K]
syntax.
Following this logic:
type ArrayType = string[];
type ElementType = ArrayType[number]; // ElementType will be of type "string"
type TupleType = [string, number, boolean];
type TupleFirstElementType = TupleType[0]; // TupleFirstElementType will be of type "string"
type TupleSecondElementType = TupleType[1]; // TupleSecondElementType will be of type "number"
Thus, T[number]
in the answer represents the type of each element in the array, and through K in T[number]
, we can obtain each type.
First of Array#
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
The idea for this question is to determine the type of the first element of the array, where the element types can be arbitrary (there's no requirement to be an Object Key).
If we directly take the first element:
type First<T extends any[]> = T[0]
However, we need to check if the array is empty, which can be done in two ways.
The simplest way is:
type First<T extends any[]> = T extends [] ? never : T[0]
But TS types can also be treated as values, so we can compute T['length']
as well. Therefore:
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
Another approach is to decompose the array, but I can't think of it 😓.
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never
Length of Tuple#
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
This can be done by directly taking the length
property, but it can also be non-readonly
. I guess it was added to avoid affecting the original array.
type Length<T extends readonly any[]> = T['length']
Exclude#
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
This is probably the hardest to understand. The answer is:
type MyExclude<T, U> = T extends U ? never : T;
Clearly, extends
is only used to check if T can be assigned to U type.
But the actual execution process is:
'a' extends 'a' ? never : 'a'
, returns never'b' extends 'a' ? never : 'b'
, returns'b'
'c' extends 'a' ? never : 'c'
, returns'c'
Finally, the result concatenates, naturally returning 'b' | 'c'
.
Refer to this article: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types.
Awaited#
This is also quite difficult to understand 🫠.
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
At first glance, this seems simple. I directly wrote:
type MyAwaited<T> = T extends Promise<infer U> ? U : never;
But have you considered that
Promise
might be nested within anotherPromise
?
So we need to improve it (and yes, types actually support recursion):
type MyAwaited<T extends Promise<any>> = T extends Promise<infer U>
? U extends Promise<any>
? MyAwaited<U>
: U
: never;
Theoretically, this should work, but I saw a highly upvoted answer as follows:
type Thenable<T> = {
then: (onfulfilled: (arg: T) => unknown) => unknown;
}
type MyAwaited<T extends Thenable<any> | Promise<any>> = T extends Promise<infer Inner>
? Inner extends Promise<any> ? MyAwaited<Inner> : Inner
: T extends Thenable<infer U> ? U : false
This left me a bit confused. Why define an additional Thenable
type and check this case? I haven't figured it out yet 🤔. If we don't define Thenable
, we could directly use PromiseLike
, which I will study later.
If#
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
This is quite simple. The first value must extends boolean
, and it needs to check whether it can be assigned to true
type (note that directly writing C ? T :F
won't work 🤣).
type If<C extends boolean, T, F> = C extends true ? T : F;
Concat#
type Result = Concat<[1], [2]> // expected to be [1, 2]
You might not expect that array types actually support the ...
operator.
type Concat<T extends any[], U extends any[]> = [...T, ...U]
Includes#
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
This looks like it requires writing a loop, but actually, based on extends
, if it checks whether it can be converted to a union type, it will expand and check one by one, i.e.,
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
You can see that T[number]
is actually:
'Kars' | 'Esidisi' | 'Wamuu' | 'Santana'
Then U extends T[number]
will check each one.
Push#
type Result = Push<[1, 2], '3'> // [1, 2, '3']
This is quite simple; just spread T and add U.
type Push<T extends any[], U> = [...T, U]
Unshift#
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
This is straightforward, so I won't elaborate.
type Unshift<T extends any[], U> = [U, ...T]
Parameters#
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
This is also quite simple; just clarify the relationships.
type MyParameters<T extends (...args:any) => any> = T extends (...args:infer P) => any ? P : never;