Type compatibility rules
TypeScript's type compatibility rules are a core concept in the type system, determining whether different types can be assigned or substituted for one another. It is based on structural typing rather than nominal typing, which makes type checking more flexible but can also lead to some unexpected behaviors.
Basic Principles of Type Compatibility
TypeScript employs a structural type system, meaning that as long as two types have the same structure, they are compatible. This contrasts with nominal type systems (like Java). For example:
interface Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
const person: Person = new Employee(); // Compatible because Employee includes all properties of Person
Here, the Employee
class contains all the properties of the Person
interface, so an instance of Employee
can be assigned to a variable of type Person
.
Function Type Compatibility
Function type compatibility rules are more complex, involving parameter types and return types.
Parameter Type Compatibility
Function parameters follow "bivariant" rules: the parameter types of the target function must be subtypes or the same types as those of the source function.
type Handler = (event: Event) => void;
const clickHandler: Handler = (e: MouseEvent) => console.log(e.button); // Compatible
Although MouseEvent
is a subtype of Event
, TypeScript allows this assignment, known as "parameter bivariance."
Return Type Compatibility
Return types follow "covariant" rules: the return type of the target function must be a subtype or the same type as that of the source function.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
function getAnimal(): Animal {
return { name: "Animal" };
}
function getDog(): Dog {
return { name: "Dog", breed: "Labrador" };
}
const animalFunc: () => Animal = getDog; // Compatible
const dogFunc: () => Dog = getAnimal; // Incompatible
Object Type Compatibility
Object type compatibility is checked recursively, requiring all properties of the target type to exist in the source type and be compatible.
Excess Property Checks
When directly assigning object literals, TypeScript performs additional checks:
interface Point {
x: number;
y: number;
}
const p: Point = { x: 1, y: 2, z: 3 }; // Error: Object literal may only specify known properties
This check can be bypassed using type assertions or intermediate variables:
const p1: Point = { x: 1, y: 2, z: 3 } as Point; // Type assertion
const temp = { x: 1, y: 2, z: 3 };
const p2: Point = temp; // Using an intermediate variable
Class Type Compatibility
Class compatibility is similar to objects but requires attention to static and private members:
class A {
private secret: string;
constructor(public name: string) {}
}
class B {
private secret: string;
constructor(public name: string) {}
}
const a: A = new B("test"); // Error: Private properties are incompatible
Even if two classes have the same structure, they are incompatible if they have private or protected members from different declarations.
Generic Type Compatibility
Generic type compatibility depends on the compatibility of their type parameters:
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string> = x; // Compatible because Empty<T> doesn't use T
interface NotEmpty<T> {
data: T;
}
let a: NotEmpty<number>;
let b: NotEmpty<string> = a; // Incompatible
Enum Type Compatibility
Enum types are compatible with numeric types but incompatible with other enum types:
enum Status { Ready, Waiting }
enum Color { Red, Blue, Green }
let s = Status.Ready;
s = 1; // Compatible
s = Color.Red; // Incompatible
Advanced Type Compatibility
Union Types
Union type compatibility checks must consider all possible types:
type U = string | number;
let u: U = "hello"; // Compatible
u = 42; // Compatible
u = true; // Incompatible
Intersection Types
Intersection types require satisfying all constituent types:
interface A {
a: number;
}
interface B {
b: string;
}
type C = A & B;
const c: C = { a: 1, b: "test" }; // Compatible
const c2: C = { a: 1 }; // Incompatible, missing property b
Type Parameter Compatibility
For generic functions, type parameters affect compatibility:
let identity = function<T>(x: T): T {
return x;
};
let reverse = function<U>(y: U): U {
return y;
};
identity = reverse; // Compatible because type parameters are only used internally
Optional and Rest Parameters
Optional and rest parameters in functions also affect compatibility:
function foo(x: number, y?: number) {}
function bar(x: number) {}
function baz(x: number, ...rest: number[]) {}
foo = bar; // Compatible
bar = foo; // Compatible
baz = foo; // Compatible
foo = baz; // Compatible
Function Overload Compatibility
For overloaded functions, each overload signature in the source function must find a compatible signature in the target function:
function overload(a: number): number;
function overload(a: string): string;
function overload(a: any): any {
return a;
}
function simple(a: string | number): string | number {
return a;
}
overload = simple; // Incompatible
simple = overload; // Compatible
Index Signature Compatibility
Object type index signatures affect compatibility:
interface StringArray {
[index: number]: string;
}
let strArr: StringArray = ["a", "b"]; // Compatible
strArr = { 0: "a", 1: "b" }; // Compatible
strArr = { 0: "a", 1: 42 }; // Incompatible
Type Inference and Compatibility
TypeScript's type inference impacts compatibility judgments:
let x = [0, 1, null]; // Inferred as (number | null)[]
const y: number[] = x; // Incompatible
Contextual Typing
Contextual typing affects the compatibility of function expressions:
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); // Compatible, mouseEvent inferred as MouseEvent
};
Strict Function Type Checking
With --strictFunctionTypes
enabled, function parameter type checks become stricter:
type Methodish = {
func(x: string | number): void;
};
const m: Methodish = {
func: (x: string) => console.log(x) // Incompatible (in strict mode)
};
Type Guards and Compatibility
Type guards affect type compatibility judgments:
function isString(x: any): x is string {
return typeof x === "string";
}
function example(x: string | number) {
if (isString(x)) {
const s: string = x; // Compatible
}
}
Mapped Type Compatibility
Mapped types preserve the compatibility relationships of the original types:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface Person {
name: string;
age: number;
}
const readonlyPerson: Readonly<Person> = { name: "Alice", age: 30 };
const person: Person = readonlyPerson; // Incompatible (because properties are readonly)
Conditional Type Compatibility
Conditional type compatibility depends on the resolved type:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
"object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<number>; // "number"
const t1: T1 = "string"; // Compatible
const t2: T2 = "number"; // Compatible
const t3: T1 = "number"; // Incompatible
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:枚举类型的使用与限制
下一篇:类与继承语法