Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

TypeScript type compatibility


May 07, 2021 TypeScript


Table of contents


TypeScript Type Compatibility Introduction

Type compatibility in TypeScript is based on structural sub-types. A structure type is a way to describe a type using only its members. I t is in contrast to the nominal type. I n a type system based on a nominal type, the compatibility or ethonability of a data type is determined by an explicit declaration and/or the name of the type.) T his is different from a structural type system in that it is a type-based composition and does not require explicit declaration. See the following example:

interface Named {
    name: string;
}

class Person {
    name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

In languages that are based on nominal types, such as C# or Java, this code is error-positive because theErson class does not explicitly state that it implements the Named interface.

TypeScript's structural subsypes are designed based on typical writing of JavaScript code. Because anonymous objects, such as function expressions and object literals, are widely used in JavaScript, it is better to use structure type systems to describe these types than to use nominal type systems.

Notes on reliability

TypeScript's type system allows certain operations that cannot be confirmed during the compilation phase. W hen a type system has this property, it is described as "unreliable." T ypeScript allows this unreliable behavior to occur after careful consideration. Through this article, we will explain when this will happen and its positive side.

Begin

The basic rule of typeScript structured type systems is that x y y least the same x as x. Like what:

interface Named {
    name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;

Here you check whether y be assigned to x and the x property in x to see if the corresponding property can also be found in y I n this example, y contain a name with a string name. y the criteria, so the assignment is correct.

Use the same rules when examining function parameters:

function greet(n: Named) {
    alert('Hello, ' + n.name);
}
greet(y); // OK

Note that y additional location but this does not cause an error. Only members of the target type Named are checked for compatibility one by one.

This comparison process is recursive, checking each member and child member.

Compare the two functions

Relatively speaking, it is easier to understand when comparing the original type and the object type, and the question is how to judge that the two functions are compatible. Let's start with two simple functions, which are just a slightly different list of parameters:

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

To see x can be assigned to y look at their list of parameters. x parameter of x must be able to find the parameter of the corresponding type in y I t doesn't matter if the names of the parameters are the same or not, just their type. Here, each parameter of x can be found y so the assignment is allowed.

The second assignment error because y has y second argument, but x does x assignment is not allowed.

You may wonder why the parameter 忽略 to be ignored, y = x s x. T he reason is that ignoring additional parameters is common in JavaScript. F or example, Array#forEach sends three parameters to the callback function: array elements, indexes, and the entire array. Nevertheless, it is useful to pass in a callback function that uses only the first argument:

let items = [1, 2, 3];

// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));

Here's a look at how to handle return value types, creating two functions that are only different in return value types:

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error because x() lacks a location property

The type system forces the return value type of the source function to be a sub-type of the return value type of the target function.

Function parameters are co-variable in both directions

When comparing function parameter types, the value is successful only if the source function argument can be assigned to the target function or vice versa. T his is unstable because the caller may have passed in a function with more precise type information, but the incoming function is called with less precise type information. I n fact, this rarely results in errors and enables the implementation of many common patterns in JavaScript. For example:

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

Optional and remaining parameters

When comparing function compatibility, optional and required parameters are interchangeable. Additional optional parameters on the original type do not cause errors, nor do optional parameters on the target type have corresponding parameters.

When a function has an argument left, it is used as an infinite number of optional arguments.

This is unstable for a type system, but from a runtime perspective, optional parameters are generally not mandatory because for most functions it is undefinded

In a good example, a common function receives a callback function and calls it with parameters that are predictable for the programmer but are uncertain for the type system:

function invokeLater(args: any[], callback: (...args: any[]) => void) {
    /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

The function is overloaded

For functions with overloads, each overload of the source function finds the corresponding function signature on the target function. This ensures that the target function can be called where all source functions can be called.

Enumeration

The enumeral type is compatible with the numeric type, and the numeric type is compatible with the enumeral type. D ifferent enumerity types are incompatible. Like what

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  //error

Class

Classes are similar to objects in character and interface, but with one thing in common: classes have the type of static and instance parts. W hen you compare objects of two class types, only the members of the instance are compared. Static members and constructors are not within the scope of the comparison.

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  //OK
s = a;  //OK

The private member of the class

Private members affect compatibility judgment. W hen an instance of a class is used to check compatibility, if it contains a private member, the target type must contain that private member from the same class. This allows children to be assigned to the parent class, but not to other classes of the same type.

Generic

Because TypeScript is a structured type system, type parameters affect only the type of result that is used as part of the type. Like what

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // okay, y matches structure of x

In the code above, x y are compatible because their structures are no different when using type parameters. By changing this example to one member, you can see how it works:

interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // error, x and y are not compatible

Here, generic types are used as if they were not generic types.

When generic parameters of generic types are not specified, all generic parameters are any The result types are then compared, as in the first example above.

Like what

let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // Okay because (x: any)=>any matches (y: any)=>any

Advanced topics

Sub-types and assignments

So far, we've 兼容性 which is not defined in the language specification. I n TypeScript, there are two types of compatibility: subtypes and assignments. The difference is that assignments extend sub-type compatibility, any any and allowing numbers to be assigned to enumeration or enumeration types.

The mechanisms in them are used in different parts of the language. I n fact, type compatibility is controlled by assignment compatibility even implements extends statements. For more information, see typeScript language specifications .