Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

TypeScript Logical Operators

Conditional Types#

type LiteralType<T> = T extends string ? "string" : "other";

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "other"
export type LiteralType<T> = T extends string
	? "string"
	: T extends number
	? "number"
	: T extends boolean
	? "boolean"
	: T extends null
	? "null"
	: T extends undefined
	? "undefined"
	: never;

type Res1 = LiteralType<"linbudu">; // "string"
type Res2 = LiteralType<599>; // "number"
type Res3 = LiteralType<true>; // "boolean"

In conditional types, the extends keyword is used to check the compatibility of types, rather than their strict equality. This is because at the type level, for two variables that can be assigned to each other, we do not need their types to be exactly the same, we only need them to be compatible, and two identical types naturally satisfy the extends condition.

function universalAdd<T extends number | bigint | string>(
	x: T,
	y: T
): LiteralToPrimitive<T> {
	return x + (y as any);
}

export type LiteralToPrimitive<T> = T extends number
	? number
	: T extends bigint
	? bigint
	: T extends string
	? string
	: never;

universalAdd("linbudu", "599"); // string
universalAdd(599, 1); // number
universalAdd(10n, 10n); // bigint
type Func = (...args: any[]) => any;

type FunctionConditionType<T extends Func> = T extends (
  ...args: any[]
) => string
  ? 'A string return func!'
  : 'A non-string return func!';

//  "A string return func!"
type StringResult = FunctionConditionType<() => string>;
// 'A non-string return func!';
type NonStringResult1 = FunctionConditionType<() => boolean>;
// 'A non-string return func!';
type NonStringResult2 = FunctionConditionType<() => number>;

infer Keyword#

In TypeScript, the infer keyword is used to extract a certain part of the type information in conditional types. For example, if we want to extract the return type of a function, we can do it like this:

type FunctionReturnType<T extends Func> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

infer is short for inference, and in infer R, R represents the type to be inferred. infer can only be used in conditional types because we still need the type structure to be consistent. In the above example, the type information needs to be a function type structure in order for us to extract its return type. If it is not a function type, I will only give you never.

The type structure here is not limited to function type structures, it can also be arrays:

type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;

type SwapResult1 = Swap<[1, 2]>; // [2, 1]
type SwapResult2 = Swap<[1, 2, 3]>; // [1, 2, 3]
// Extract the first and last elements
type ExtractStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...any[],
  infer End
]
  ? [Start, End]
  : T;

// Swap the first and last elements
type SwapStartAndEnd<T extends any[]> = T extends [
  infer Start,
  ...infer Left,
  infer End
]
  ? [End, ...Left, Start]
  : T;

// Swap the first two elements
type SwapFirstTwo<T extends any[]> = T extends [
  infer Start1,
  infer Start2,
  ...infer Left
]
  ? [Start2, Start1, ...Left]
  : T;
type ArrayItemType<T> = T extends Array<infer ElementType> ? ElementType : never;

type ArrayItemTypeResult1 = ArrayItemType<[]>; // never
type ArrayItemTypeResult2 = ArrayItemType<string[]>; // string
type ArrayItemTypeResult3 = ArrayItemType<[string, number]>; // string | number
// Extract the type of a property of an object
type PropType<T, K extends keyof T> = T extends { [Key in K]: infer R }
  ? R
  : never;

type PropTypeResult1 = PropType<{ name: string }, 'name'>; // string
type PropTypeResult2 = PropType<{ name: string; age: number }, 'name' | 'age'>; // string | number

// Reverse the key-value pairs
type ReverseKeyValue<T extends Record<string, unknown>> = T extends Record<infer K, infer V> ? Record<V & string, K> : never

type ReverseKeyValueResult1 = ReverseKeyValue<{ "key": "value" }>; // { "value": "key" }
type PromiseValue<T> = T extends Promise<infer V> ? PromiseValue<V> : T;

type PromiseValueResult1 = PromiseValue<Promise<number>>; // number
type PromiseValueResult2 = PromiseValue<number>; // number, no extraction occurred

Distributive Conditional Type#

For checking types that belong to naked type parameters, conditional types will automatically distribute to union types during instantiation. (Whether passed in through generic parameters)

type Condition<T> = T extends 1 | 2 | 3 ? T : never;

// 1 | 2 | 3
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;

// never
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;
type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Res3 = Naked<number | boolean>;

// "N"
type Res4 = Wrapped<number | boolean>;

To summarize the clues above, we can roughly understand the conditions for the distributive nature of conditional types. First, your type parameter needs to be a union type. Second, the type parameter needs to be passed in through generic parameters, rather than directly used for conditional type checking (as in Res2). Finally, the generic parameter in the conditional type cannot be wrapped.

The effect of the distributive nature of conditional types is also evident, which is to split the union type, perform a conditional type check on each branch, and then merge the final result (as in Naked).

However, it is not necessary to ensure the distributive nature by exposing the generic parameter. In some cases, we also need to wrap the generic parameter to disable the distributive nature. Perhaps the most common scenario is the check of union types, where we do not want to distribute the members of the union type, but want to directly check the compatibility of these two union types, just like in the initial Res2:

type CompareUnion<T, U> = [T] extends [U] ? true : false;

type CompareRes1 = CompareUnion<1 | 2, 1 | 2 | 3>; // true
type CompareRes2 = CompareUnion<1 | 2, 1>; // false

Another scenario is when we want to check if a type is never, we can also use a similar approach:

type IsNever<T> = [T] extends [never] ? true : false;

type IsNeverRes1 = IsNever<never>; // true
type IsNeverRes2 = IsNever<"linbudu">; // false
type IsAny<T> = 0 extends 1 & T ? true : false;

Intersection types, like the short board effect, calculate the type based on the shortest board, which is the most accurate type.

type IsUnknown<T> = unknown extends T
  ? IsAny<T> extends true
    ? false
    : true
  : false;

It should be noted that the cases of never and any are not exactly the same. any will produce this effect when used directly as a judging parameter or as a generic parameter:

// Returns a union type
type Tmp1 = any extends string ? 1 : 2;  // 1 | 2

type Tmp2<T> = T extends string ? 1 : 2;
// Returns a union type as well
type Tmp2Res = Tmp2<any>; // 1 | 2

// If the condition to be checked is `any`, the check will still be performed
type Special1 = any extends any ? 1 : 2; // 1
type Special2<T> = T extends any ? 1 : 2;
type Special2Res = Special2<any>; // 1

never only produces this effect when used as a generic parameter:

// Check is still performed when used directly
type Tmp3 = never extends string ? 1 : 2; // 1

type Tmp4<T> = T extends string ? 1 : 2;
// Check is skipped when passed as a generic parameter
type Tmp4Res = Tmp4<never>; // never

// If the condition to be checked is `never`, the check will be skipped only when it is used as a generic parameter
type Special3 = never extends never ? 1 : 2; // 1
type Special4<T> = T extends never ? 1 : 2;
type Special4Res = Special4<never>; // never

References:
https://juejin.cn/book/7086408430491172901

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.