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

This article will be published synchronously in my personal blog, welcome to subscribe and exchange.

Extends for condition determination

When extends is used to represent a conditional judgment, the following rules can be summarized

  1. If the type on both sides of extends is the same, extends is semantically understood as= = =, please refer to the following examples:
type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false     // false
Copy the code
  1. The result is true if a type to the right of extends contains a type to the left of extends (that is, a narrow type extends a broad type), and false if the type to the right of extends extends. Consider the following examples:
type result3 = string extends string | number ? true : false // true
Copy the code
  1. When extends extends to an object, the more keys you specify in the object, the narrower the scope of its type definition. Consider the following examples:
type result4 = { a: true.b: false } extends { a: true}?true : false // true
Copy the code

Use conditional types in generic types

Consider the following Demo type definition:

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

Combining extends, when used in conditional known ‘a’ | ‘b’ | ‘c’ extends’ a ‘is false, so the Demo <‘ a ‘|’ b ‘|’ c ‘, ‘a’ > the result is’ a ‘|’ b ‘|’ c ‘?

Check out the official website, which mentions:

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

That is, when a conditional type applies to a generic type, the union type is split and used. The Demo < ‘a’ | ‘b’ | ‘c’, ‘a’ > will be broken up into ‘a’ extends’ a ‘, ‘b’ extends’ a ‘, ‘c’ extends’ a ‘. Using pseudocode to represent something like:

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

Demo(['a'.'b'.'c'].'a') // ['never', 'b', 'c']
Copy the code

Furthermore, according to the definition of the never type — the never type can be assigned to every type, but no type can be assigned to never(except never itself). That never | ‘b’ | ‘c’ is equivalent to ‘b’ | ‘c’.

So Demo < ‘a’ | ‘b’ | ‘c’, ‘a’ > is not the result of the ‘a’ | ‘b’ | ‘c’ instead of ‘b’ | ‘c’.

Tool type

As you might have noticed, the process of declaring Demo types is how Exclude

is implemented in TypeScript’s official utility types, It is used to exclude the union Type ExcludedUnion from the Type Type.
,>

type T = Demo<'a' | 'b' | 'c'.'a'> // T: 'b' | 'c'
Copy the code

Based on the Demo Type definition, we can further implement the official tool Type of Omit

, which is used to remove property values that meet the Keys Type in object 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 }
Copy the code

Escape capsule

If you want the Demo < ‘a’ | ‘b’ | ‘c’, ‘a’ > as the result of the ‘a’ | ‘b’ | ‘c’ if you can achieve? According to the official website:

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 through every type in a generic type, you can enclose the generic type in square brackets to indicate the whole part that uses it.

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

/ / the result type at this time for the 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c'.'a'>
Copy the code

Use conditional types in arrow functions

When using a ternary expression in an arrow function, the left-to-right reading habit results in confusing the user without parentheses in the function content area. For example, is x a function or a Boolean in the code below?

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

In esLint rule no-confusing-arrow, it is recommended to write as follows:

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

The same goes for extends in arrow functions in TypeScript type definitions, and the left-to-right reading habit leads to confusion about the order in which type code is executed.

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

So using extends in arrow functions with parentheses is a great help for code review.

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

Derive the use condition type from the type

In TypeScript, the infer syntax is typically used with extends. It can be used for the purpose of automatically deriving types. For example, it implements the utility Type ReturnType

, which returns the ReturnType of the function Type.

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

MyReturnType<() = > string>          // string
MyReturnType<() = > Promise<boolean> // Promise<boolean>
Copy the code

Combining extends with type derivation, you can also implement array-related Pop

, Shift

, and Reverse

utility types.


Pop<T>:

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

type T = Pop<[3.2.1] >// T: [3, 2]
Copy the code

Shift<T>:

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

type T = Shift<[3.2.1] >// T: [2, 1]
Copy the code

Reverse<T>

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

type T = Reverse<['a'.'b'] >// T: ['b', 'a']
Copy the code

Use conditional types to determine that two types are exactly equal

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

Plan 1: Refer to the issue.

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

The only drawback of the current scheme is that the any type is judged equal to any other type.

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

Plan 2: Refer to the issue.

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

The only drawback of the current scheme is a slight flaw in the handling of crossover types.

type T = Equal2<{x:1} and {y:2}, {x:1.y:2} >// false
Copy the code

The above two kinds of judgment type equal method has different opinions, the author throws out a brick to attract jade here.