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

5 super useful open source Docker tools highly recommended

Introduction The Docker community has created man...

CSS animation combined with SVG to create energy flow effect

The final effect is as follows: The animation is ...

A brief talk about React Router's history

If you want to understand React Router, you shoul...

MySQL password modification example detailed explanation

MySQL password modification example detailed expl...

Detailed explanation of vue-router 4 usage examples

Table of contents 1. Install and create an instan...

Solve the problem of PhPStudy MySQL startup failure under Windows system

Report an error The Apache\Nginx service started ...

15-minute parallel artifact GNU Parallel Getting Started Guide

GNU Parallel is a shell tool for executing comput...

Upgrade MySQL 5.1 to 5.5.36 in CentOS

This article records the process of upgrading MyS...

Detailed explanation of several methods of deduplication in Javascript array

Table of contents Array deduplication 1 Double-la...

Tutorial on installing MySQL 5.7.18 decompressed version on Windows

1. Installation process MySQL version: 5.7.18 1. ...

HTML Tutorial: title attribute and alt attribute

XHTML is the basis of CSS layout. jb51.net has al...

Web developers are concerned about the coexistence of IE7 and IE8

I installed IE8 today. When I went to the Microso...

10 Deadly Semantic Mistakes in Web Typography

<br />This is from the content of Web front-...