In-depth reading and practice records of conditional types in TypeScript

In-depth reading and practice records of conditional types in TypeScript

In most programs, we have to make decisions based on input. TypeScript is no exception, using conditional types you can describe the relationship between input types and output types.

Used for conditional judgment

When extends is used to express conditional judgment, the following rules can be summarized:

If the types on both sides of extends are the same, extends can be understood semantically as ===, as shown in the following example:

type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false // false

If the type on the right side of extends contains the type on the left side of extends (that is, the narrow type extends the broad type), the result is true, otherwise it is false. You can refer to the following example:

type result3 = string extends string | number ? true : false // true

When extends is applied to an object, the more keys are specified in the object, the narrower the scope of its type definition. You can refer to the following example:

type result4 = { a: true, b: false } extends { a: true } ? true : false // true

Using conditional types in generic types

Consider the following Demo type definition:

type Demo<T, U> = T extends U ? never : T

Combined with the extends used in conditional judgment, we can know that 'a' | 'b' | 'c' extends 'a' is false, so the result of Demo<'a' | 'b' | 'c', 'a'> is 'a' | 'b' | 'c'?
Check the official website, which mentions:

When conditional types act on a generic type, they become distributive when given a union type.

That is, when the conditional type acts on a generic type, the union type will be split and used. That is, Demo<'a' | 'b' | 'c', 'a'> will be split into 'a' extends 'a', 'b' extends 'a', 'c' extends 'a'. In pseudocode it is similar to:

function Demo(T, U) {
  return T.map(val => {
    if (val !== U) return val
    return 'never'
  })
}

Demo(['a', 'b', 'c'], 'a') // ['never', 'b', 'c']

Furthermore, according to the definition of the never type - the never type is assignable to every type, but no type is assignable to never (except never itself). That is, never | 'b' | 'c' is equivalent to 'b' | 'c'.

Therefore, the result of Demo<'a' | 'b' | 'c', 'a'> is not 'a' | 'b' | 'c' but 'b' | 'c'.

Tool Type

Careful readers may have discovered that the declaration process of the Demo type is actually the implementation principle of Exclude<Type, ExcludedUnion> in the tool type officially provided by TypeScript, which is used to exclude the union type ExcludedUnion from the Type type.

type T = Demo<'a' | 'b' | 'c', 'a'> // T: 'b' | 'c'

Based on the Demo type definition, you can further implement Omit<Type, Keys> in the official tool type, which is used to remove the object Type
The attribute value that satisfies the keys type.

type Omit<Type, Keys> = {
  [P in Demo<keyof Type, Keys>]: Type<P>
}

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type T = Omit<Todo, 'description'> // T: { title: string; completed: boolean }

Escape Pod

If you want the result of Demo<'a' | 'b' | 'c', 'a'> to be 'a' | 'b' | 'c', is it possible? According to the official website description:

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

If you don't want to iterate over every type in the generic, you can enclose the generic in square brackets to indicate the whole part that uses the generic.
type Demo<T, U> = [T] extends [U] ? never : T

type Demo<T, U> = [T] extends [U] ? never : T

// result is of type 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c', 'a'>

Using conditional types in arrow functions

When using ternary expressions in arrow functions, the left-to-right reading habit makes the function content area confusing if it is not enclosed in parentheses. For example, in the following code, is x a function type or a Boolean type?

// The intent is not clear.
var x = a => 1 ? true : false

In the eslint rule no-confusing-arrow, the following is recommended:

var x = a => (1 ? true : false)

In TypeScript type definitions, if extends is used in arrow functions, the same is true. Due to the left-to-right reading habit, readers may be confused about the execution order of type codes.

type Curry<P extends any[], R> =
  (arg: Head<P>) => HasTail<P> extends true ? Curry<Tail<P>, R> : R

Therefore, it is recommended to add parentheses when using extends in arrow functions, which is very helpful for code review.

type Curry<P extends any[], R> =
  (arg: Head<P>) => (HasTail<P> extends true ? Curry<Tail<P>, R> : R)

Using conditional types with type inference

In TypeScript, the type infer syntax is usually used in conjunction with extends. Use it to achieve the purpose of automatically inferring types. For example, it can be used to implement the tool type ReturnType<Type>, which is used to return the return type of the function Type.
type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

MyReturnType<() => string> // string
MyReturnType<() => Promise<boolean> // Promise<boolean>

Combining extends with type inference can also implement array-related Pop<T>, Shift<T>, and Reverse<T> tool types.

Pop<T>:

type Pop<T extends any[]> = T extends [...infer ExceptLast, any] ? ExceptLast : never

type T = Pop<[3, 2, 1]> // T: [3, 2]

Shift<T>:

type Shift<T extends any[]> = T extends [infer _, ...infer O] ? O : never

type T = Shift<[3, 2, 1]> // T: [2, 1]

Reverse<T>

type Reverse<T> = T extends [infer F, ...infer Others]
  ? [...Reverse<Others>, F]
  : []

type T = Reverse<['a', 'b']> // T: ['b', 'a']

Use conditional types to determine if two types are completely equal

We can also use conditional types to determine whether types A and B are completely equal. There are currently two main solutions in the community:

Solution 1: Refer to issue.

export type Equal1<T, S> =
  [T] extends [S] ? (
    [S] extends [T] ? true : false
  ) : false

The only drawback of this solution at present is that it will judge any type as equal to any other type.

type T = Equal1<{x:any}, {x:number}> // T: true

Solution 2: Refer to issue.

export type Equal2<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<U>() => U extends Y ? 1 : 2) ? true : false

The only shortcoming of this solution is that it has some flaws in the handling of intersection types.

type T = Equal2<{x:1} & {y:2}, {x:1, y:2}> // false

The above two methods of judging whether types are equal are subject to different opinions, and the author is just throwing out some ideas to stimulate discussion.

Summarize

This concludes this article on the intensive reading and practice of conditional types in TypeScript. For more relevant TypeScript conditional types content, please search for previous articles on 123WORDPRESS.COM or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • TypeScript enumeration basics and examples
  • Typescript+react to achieve simple drag and drop effects on mobile and PC
  • TypeScript problem with iterating over object properties

<<:  6 solutions to IDEA's inability to connect to the MySQL database

>>:  Detailed steps to install Nginx on Linux

Recommend

How to count the number of specific characters in a file in Linux

Counting the number of a string in a file is actu...

How to use LibreOffice to convert document formats under CentOS

Project requirements require some preprocessing o...

jQuery implements the mouse drag image function

This example uses jQuery to implement a mouse dra...

Detailed explanation of Linux one-line command to process batch files

Preface The best method may not be the one you ca...

Install JDK1.8 in Linux environment

Table of contents 1. Installation Environment 2. ...

Detailed usage of React.Children

Table of contents 1. React.Children.map 2. React....

Detailed explanation of the principle of creating tomcat in Eclipse

When creating a tomcat server on a local eclipse,...

How to mark the source and origin of CSS3 citations

I am almost going moldy staying at home due to th...

Detailed explanation of Javascript Echarts air quality map effect

We need to first combine the air quality data wit...

Perform data statistics on different values ​​of the same field in SQL

Application scenario: It is necessary to count th...

MySQL knowledge points for the second-level computer exam mysql alter command

Usage of alter command in mysql to edit table str...

Detailed explanation of how to enter and exit the Docker container

1 Start the Docker service First you need to know...

A brief description of the relationship between k8s and Docker

Recently, the project uses kubernetes (hereinafte...