阿里云主机折上折
  • 微信号
Current Site:Index > Type compatibility rules

Type compatibility rules

Author:Chuan Chen 阅读数:40908人阅读 分类: TypeScript

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.