The reason for this article is that some strange behaviors occur during type gymnastics. The title link is here, interested friends can have a look at the title, the last will come to type exercise.
The problem
The short description of this bizarre behavior is
type a = never extends never ? true : false // true
type case1<T = never> = never extends T ? true : false
type case2<T = never> = T extends never ? true : false
type b = case1 // true
type c = case2 // never
Copy the code
Extends never normally extends never, as shown in type A and type B, and the result is true. But in type C, T extends Never? True: false is theoretically instantiated to never extends never? True: false, expected true, but never.
To solve this problem. First, what does extends mean?
subtypes
In Typescript, the following code works: func receives arguments of type TypeA, and obj is passed into Func as a literal object of type TypeB. We can call TypeB a subtype of TypeA.
interface TypeA {
a: string;
}
interface TypeB {
a: string;
}
function func(x: TypeA) {}
const obj: TypeB = { a: 'a' };
func(obj) // OK
Copy the code
From the extends perspective, if TypeA can extends TypeB, then TypeA is a subtype of TypeB (son :D).
Nominal subtype
In some other programming languages, to create a subtype of TypeA, TypeB, you must explicitly declare this parent-child relationship. Such as
interface TypeB extends TypeA {}
Copy the code
If not explicitly declared, the compiler will report an error. This is the nominal subtype
Structure subtype
In Typescript, there is no need for explicit declarations, just structural similarity. We call these structural subtypes.
For example, TypeA and TypeB, the structure is exactly the same, their parent-child relationship is mutual, can say that TypeA is a subtype of TypeB, can also say that TypeB is a subtype of TypeA.
If TypeB is rewritten as follows, then TypeA cannot be a subtype of TypeB, that is, the subtype must have no more attributes than the parent.
interface TypeB {
a: string;
b: string;
}
Copy the code
Conditional Types
Conditional Types are Conditional Types, which look like this. TrueType is generated when SomeType is a subtype of OtherType, and FalseType otherwise.
SomeType extends OtherType ? TrueType : FalseType;
Copy the code
Concrete example
type A = 1 extends 1 ? true : false; // true
Copy the code
But that still doesn’t explain the following situation. There is no TrueType or FalseType.
type case2<T = never> = T extends never ? true : false
type c = case2 // never
Copy the code
Distributive Conditional Types
When a Conditional type occurs ona generic type, it becomes Distributive Conditional Types. For example
type Distributive<T> = T extends unknown ? [T] : never;
type A = Distributive<string | number>;
Copy the code
According to the above logic reasoning, remove the generic, A can be converted to the following content, the final result is [string | number]
type A = string | number extends unknown ? [string | number] : never;
Copy the code
But the actual operation shows that the result of A is [string] | [number].
The official documentation is as follows
When conditional types act on a generic type, they become distributive when given a union type.
Because of the type A = Distributive < string | number >, introduced into A Union type, so after removing actually generic, A long.
type A = (string extends unknown ? [string] : never) | (number extends unknown ? [number] : never)
Copy the code
Note that Distributive Conditional Types are triggered by the following conditions, as exemplified by the code below.
type Distributive<T> = T extends unknown ? [T] : never;
Copy the code
-
Generic parameters must be on the left side of extends, not the right. Type Distributive
= unknown extends T? [T] : never
-
Must be naked type. It can be understood as a bare type. In plain English, it means that no modification can be made to the generic parameter. It must be pure T. Such as the type Distributive < T > = T | 1 extends unknown? [T] : never in T | 1 is no longer simple.
-
Must be Union type
conclusion
So how do you interpret this code
type case2<T = never> = T extends never ? true : false
type c = case2 // never
Copy the code
As we all know, never is a non-existent type in typescript. The Union type represents a set, and never means an empty set. In the case of Distributive Conditional Types, extends can represent the meaning of traversing a collection. Traversing an empty set of never naturally yields an empty never.
Then some viewers have to ask, why is “never” also a Union type? Never actually | operation identity element. All types can be understood as Union types.
For example, the type of U < T > = T | never, we’re going to get U is of type T, easy to understand that you have a type T, at this time also has a non-existent thing never, so eventually you hand is T. So T and T | never is identical.
So how to solve this problem? Even if Distributive Conditional Types are not triggered, the easiest way is not to make T naked type. The following code, as long as we wrap T.
type case2<T = never> = [T] extends [never] ? true : false
type c = case2 // true
Copy the code
The last
Back to the original title. They’re linked here. The answer is linked here.
The first parameter is [1, 2, NULL, 3], and the second parameter is null. The first parameter is [1, 2, null, 3]. This is the filter in JS. So our logic is simple: go through the list of elements, filter them if they are subtypes of the second argument, and leave them if they are not.
type FilterOut<T extends any[], F, R extends any[] = []> =
T extends [infer H, ...infer P]
? ([H] extends [F] ? FilterOut<P, F, R> : FilterOut<P, F, [...R, H]>)
: R
Copy the code
Let’s explain this code step by step
-
First T extends [infer H,…infer P] The conditional type can be inferred from the clause of extends, where H is the first element and P is the rest of the array.
-
We initialized an empty array R as a result of FilterOut, which can be understood as a local variable in JS. When H is a subtype of F, that is, [H] can extends [F], we FilterOut H and process the remaining element P to FilterOut
. Otherwise, we add H to the result array R and go to FilterOut
.
,>
,> -
At the end of the recursion, our termination condition is that P is an empty array, and P is the first argument to FilterOut. When the array is empty, T extends [infer H,…infer P] fails, returning the result array R directly.