SoFunction
Updated on 2025-04-09

Examination of conditional types and practical records in TypeScript

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

Extends used for conditional judgment

When extends is used to represent 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 ===. You can refer to 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 (i.e., the narrow type extends broad type), the result is true, otherwise false. You can refer to the following example:

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

When extends acts on 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

Use 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 see that 'a' | 'b' | 'c' extends 'a' is false, so Demo<'a' | 'b' | 'c', 'a'> The result is 'a' | 'b' | 'c'?
ViewOfficial website, which mentioned:

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

That is, when the condition type acts on the 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'. Use pseudo-code to represent something similar to:

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

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

In addition, according tonever typeDefinition of ― The never type can be assigned to each type, but no type can be assigned to never (except never itself). That is, never | 'b' | 'c' is equivalent to 'b' | 'c'.

So 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 among the tool types provided by TypeScript.ExcludeThe implementation principle 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, it can further implement Omit<Type, Keys> in the official tool type, which is used to remove 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 cabin

If you want the result of Demo<'a' | 'b' | 'c', 'a'> is 'a' | 'b' | 'c'? According toOfficial websitedescribe:

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

If you do not want to iterate through each type in a generic, you can wrap the generic in square brackets to indicate the entire part of using the generic.
type Demo<T, U> = [T] extends [U] ? never : T

type Demo&lt;T, U&gt; = [T] extends [U] ? never : T

// result At this time, the type is 'a' | 'b' | 'c'type result = Demo&lt;'a' | 'b' | 'c', 'a'&gt;

Use conditional types in arrow functions

When using ternary expressions in arrow functions, the left-to-right reading habit causes the function content area to be left unpacked, which will confuse the user. For example, is x in the code below a function type or a boolean type?

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

In eslint rulesno-confusing-arrow In the following way of writing:

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

In TypeScript's type definition, the same is true for using extends in arrow functions. Due to the left-to-right reading habit, readers will be confused about the execution order of the type code.

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

Therefore, it is recommended to use extends in arrow functions to add brackets, 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)

Deduce the use of condition types in combination with type

In TypeScript, type derivation is generally used in combination with extendsinfer grammar. Use it to achieve the purpose of automatically deriving types. For example, use it to implement tool typesReturnType, this tool type 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 and type derivation 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 that the two types are exactly equal

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

Plan 1: Referenceissue

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

The only disadvantage of the current solution is that it will judge the any type to be equal to any other type.

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

Plan 2: Referenceissue

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

The only disadvantage of the current solution is that it has a little flaw in handling the cross type.

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

The above two methods of judging the same type are different, and I will give you some advice here.

Summarize

This is the article about the intensive reading and practice of condition types in TypeScript. For more related TypeScript content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!