banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

TS Challenge Record (1)

vipul-jha-a4X1cdC1QAc-unsplash

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 the any type is these three 🤣.
  • readonly (keyof any)[] represents all read-only (string | number | symbol)[], i.e., a mixed-type array.
  • [K in T[number]] uses number to represent numeric literals.

Why use readonly (keyof any)[] instead of readonly 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 another Promise?

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;
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.