Type hierarchy and bottom type
The type hierarchy and bottom type are two key concepts in TypeScript's type system. Understanding them helps in building more robust type constraints, especially when dealing with complex type relationships and edge cases. The type hierarchy determines the compatibility rules between types, while the bottom type represents the lowest-level type in the type system.
Basic Concepts of Type Hierarchy
The type hierarchy in TypeScript is similar to the inclusion relationship in set theory. When type A can be assigned to type B, we say that A is a subtype of B, or B is a supertype of A. This relationship forms the skeleton of the type system.
let a: string = "hello";
let b: String = a; // Valid, because string is a subtype of String
The hierarchy of basic types is as follows:
never
<: all types- Literal types <: corresponding primitive types
- Primitive types <: corresponding wrapper types
- All types <:
unknown
Top Type and Bottom Type
In the type hierarchy, unknown
is the top type, which can accept values of any type. On the other hand, never
is the bottom type, which cannot accept any value.
function acceptAny(value: unknown) {
// Can accept any parameter
}
function rejectAll(value: never) {
// Cannot accept any actual parameter
}
The uniqueness of the bottom type never
lies in:
- It is a subtype of all types.
- No value can be assigned to the
never
type. - A function returning
never
indicates it will never return normally.
Type Compatibility Rules
TypeScript uses a structural type system, where type compatibility is based on the structure of types rather than their declarations. This leads to some interesting hierarchical relationships:
interface Point {
x: number;
y: number;
}
interface Point3D extends Point {
z: number;
}
let p1: Point = { x: 0, y: 10 };
let p2: Point3D = { x: 0, y: 10, z: 20 };
p1 = p2; // Valid, Point3D is a subtype of Point
Hierarchy of Union Types and Intersection Types
Union types and intersection types exhibit different behaviors in the type hierarchy:
type A = { a: number };
type B = { b: string };
// Intersection types produce subtypes
type AB = A & B; // AB <: A, AB <: B
// Union types produce supertypes
type AOrB = A | B; // A <: AOrB, B <: AOrB
Practical Use Cases of never
The bottom type never
has several important applications in real-world development:
- Representing impossible branches:
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function handleShape(shape: Circle | Square) {
switch (shape.kind) {
case "circle":
return shape.radius * Math.PI;
case "square":
return shape.sideLength ** 2;
default:
return assertNever(shape); // Ensures all cases are handled
}
}
- Filtering union types:
type OnlyStrings<T> = T extends string ? T : never;
type Result = OnlyStrings<"a" | "b" | 1 | 2>; // "a" | "b"
- Representing functions that never return:
function infiniteLoop(): never {
while (true) {}
}
never
in Type Inference
In conditional type inference, never
exhibits special behavior:
type TryInfer<T> = T extends { a: infer A } ? A : never;
type Test1 = TryInfer<{ a: string }>; // string
type Test2 = TryInfer<{}>; // never
Type Hierarchy and Conditional Types
Conditional types leverage the type hierarchy to perform complex type operations:
type IsSubtype<T, U> = T extends U ? true : false;
type Test1 = IsSubtype<string, String>; // true
type Test2 = IsSubtype<"literal", string>; // true
type Test3 = IsSubtype<never, unknown>; // true
never
in Distributive Conditional Types
When never
appears in distributive conditional types, it is automatically filtered out:
type ToArray<T> = T extends any ? T[] : never;
type Test = ToArray<string | number>; // string[] | number[]
type TestNever = ToArray<never>; // never
Type Hierarchy and Mapped Types
Mapped types also exhibit special behavior when handling never
:
type FilterProperties<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
};
interface Example {
name: string;
age: number;
flag: boolean;
}
type StringProps = FilterProperties<Example, string>; // { name: string }
never
in Type Predicates
In user-defined type guards, never
can be used to represent impossible cases:
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === "string");
}
function processValue(value: string[] | number[]) {
if (isStringArray(value)) {
// value is string[]
} else {
// value is number[]
// If the isStringArray type predicate is incorrect, a never type may remain here
}
}
never
in Type Parameter Constraints
never
can be used as a type parameter constraint to indicate "no type is allowed":
function forbidden<T extends never>() {}
forbidden(); // Error: Type parameter required
forbidden<string>(); // Error: string does not satisfy never constraint
never
in Recursive Types
In recursive types, never
is often used as a termination condition:
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Test = DeepReadonly<{ a: { b: number } }>;
// { readonly a: { readonly b: number } }
Type Hierarchy and Function Types
Function parameter types are contravariant, while return types are covariant:
type Func = (arg: string) => void;
let func1: Func = (arg: string) => {};
let func2: Func = (arg: "literal") => {}; // Error
let func3: Func = (arg: unknown) => {}; // Valid
Type Hierarchy and Class Inheritance
Class inheritance relationships also follow type hierarchy rules:
class Animal {
move() {}
}
class Dog extends Animal {
bark() {}
}
let animal: Animal = new Dog(); // Valid
// animal.bark(); // Error: bark method does not exist on Animal type
Type Assertions and Hierarchy
Type assertions can bypass type checking but must satisfy certain hierarchical relationships:
let value = "hello" as unknown as number; // Not recommended, but technically possible
Type Hierarchy and Template Literal Types
Template literal types also participate in the type hierarchy:
type Color = "red" | "green" | "blue";
type UpperColor = Uppercase<Color>; // "RED" | "GREEN" | "BLUE"
let color: Color = "red";
let upperColor: UpperColor = "RED";
// color = upperColor; // Error
// upperColor = color; // Error
Type Hierarchy and Indexed Access
When accessing types via indexing, never
appears for non-existent properties:
type Example = { a: string; b: number };
type A = Example["a"]; // string
type C = Example["c"]; // Error: Property 'c' does not exist
type All = Example[keyof Example]; // string | number
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:类型谓词与自定义类型守卫
下一篇:类型编程最佳实践