An overview of the

Typescript type compatibility means that variables of different types can be interchangeable and gives the compiler additional information to identify specific types. This includes:

  • Variable type compatibility
  • Function type compatibility
  • Type the guards

Compatible with

Variable type compatibility

Typescript type compatibility is not based on type names, but on type members. That is, two types are compatible if they have the same members or if one of them lacks only alternative types. Such as:

interface A {
  name: stringvalue? :number
}
interface B {
  name: string
}

let a: A = { name: 'b' } as B
let c: stirng = 0 / / error
Copy the code

Because the required attribute of both types A and B is name, A variable of type B can be assigned to A variable of type A. But string and number are incompatible and cannot be assigned to each other.

Here’s an example:

function log(x: { name: string }) {}

let a = {
  name: 'a'.value: 0
}

log(a)
Copy the code

The x parameter requires the object to have a name member of type string. Although object A has an extra value member, as long as it has a name member of type string, the compatibility condition is satisfied.

With one exception, if you pass a direct value to a function, it must be the same as the object member of the argument. There must be no extra members, and so must the return value. Such as:

log({ name: 'a'.value: 0 }) / / error. The value spare
Copy the code

The class instance

Instances of different classes are type compatible as long as the instance members are compatible. Static member compatibility is not required.

Example:

class A {
  static value: string = 'a'

  log(x: string) :void{}}class B {
  static value: number = 0

  log(x: string) :void{}}let a: A = new B()
Copy the code

Function type compatibility

Function type compatibility mainly refers to the compatibility of function parameter number type and return value type.

Example:

type Scale = (x: string, y: string) = > { length: number }

let scale: Scale = function (x: string) :{ length: number } {
  let result = { length: x.length }

  return result
}

let v = scale('x'.'y')
Copy the code

Function parameters and return values also follow the compatibility of the previous variable types, except that there is broader compatibility, and parameters and return values are compatible in different directions. Example:

type Scale = (x: string, y: string) = > { length: number }

let scale: Scale = function (x: string) :{ length: number } {
  let result = { length: x.length }

  return result
}

let v = scale('x'.'y')
Copy the code

The Scale function takes two arguments, but the variable Scale is given a function value with only one argument, but this is legal. Arguments flow from the function to the function. The Scale type requires two arguments, but not all of them need to be used inside the function. Such types are compatible because a runtime error does not occur even if the function body uses only one argument and does not use the other. That is, a function variable’s arguments are compatible as long as they are type-compatible and have less than or equal to the number of arguments declared by the type.

Let’s look at the different return types:

type Scale = (x: string) = > { length: number }

let scale: Scale = function (x: string) :{ length: number } {
  let result = { length: x.length, value: x }

  return result
}

let v = scale('x')
Copy the code

The return value of a Scale function has only one length member, but the return value of the Scale variable has both length and value members, which is also legal. The return value flows out of the function, and the Scale type declares that the returned object has a member, which is valid as long as it meets the external expectation that the length member is returned. It is impossible for the compiler to infer whether additional members are used.

To sum up, it means that the data outflow side meets the expectation of the data inflow side and does not cause the occurrenceundefinedThe type is compatible with the runtime error.

Type the guards

Null, and undefined

Some variables use the union type to make them null (null or undefined), for example:

function scale(x: string | null) {
  return x.length / / error
}
Copy the code

Because x may be null, but null has no length member, the compiler reports an error. Compatibility can be achieved by determining whether x exists:

function scale(x: string | null) {
  if (x) {
    / / or x! == null
    return x.length
  } else {
    return 0}}Copy the code

Sometimes we can make sure that a variable of some controllable type must have a value, so we can use the operator! To tell the compiler that this variable cannot be null or undefined. Such as:

let a = document.getElementById('a')

let b = a.tagName / / error
letc = a! .tagNameCopy the code

Since variable A may be null, we cannot read the tagName attribute directly. If we are sure that the element exists, we can add! To variable A. The compiler assumes that a must have a value and does not return an error.

Multitype function parameters

Some functions have an argument type that is a union type and can pass in multiple types of arguments that are not necessarily compatible when used.

function scale(x: string | number) {
  return x.length / / error
}
Copy the code

Because the number type has no length member, Typescript reports that there may be an error. We can use typeof to determine the type here, for example:

function scale(x: string | number) {
  if (typeof x === 'string') {
    return x.length
  } else {
    return x.toString().length
  }
}
Copy the code

The Typescript compiler detects that the code is judging the type, so it is safe to read the Length property here.

You can also use instanceof to determine the prototype. Example:

function value(x: Date | string) {
  if (x instanceof Date) {
    return x.toUTCString()
  } else {
    return x.toUpperCase()
  }
}
Copy the code

Member name

The in operator determines whether a member exists, and thus the type.

Example:

interface A {
  name: string
}
interface B {
  value: number
}

function scale(x: A | B) {
  if ('name' in x) {
    return x.name.length
  } else {
    return 0}}Copy the code

Interface literal member

For types that have literal-type members, the type can be determined by judging the literal members.

Example:

interface A {
  name: 'a'
  value: string
}
interface B {
  name: 'b'
  value: number
}

function scale(x: A | B) {
  if (x.name === 'a') {
    return x.value.length
  } else {
    return 0}}Copy the code

X === ‘A’; x == ‘A’; x = ‘A’;

Custom type guards

If the above methods are not enough, we can customize the type guard by writing a judgment function. Example:

interface A {
  name: 'a'
  value: string
}
interface B {
  name: 'b'
  value: number
}

function isA(x: any) :x is A {
  return x.name === 'a'
}

function scale(x: A | B) {
  if (isA(x)) {
    return x.value.length
  } else {
    return 0}}Copy the code

We declare A custom type guard by using the is operator on the return value of isA to determine whether x is of type A. The body of the function is the concrete logic for the judgment, and the return value of the function must be Boolean. The point of this function is that we tell the compiler how to determine if an argument is of type A, which can be used in A statement such as if, so that the compiler can do type inference for the corresponding variable.

You can use this as the is operand in a class. Example:

class C implements A {
  name: 'a' = 'a'
  value: string = '0'

  isA(): this is A {
    return this.name === 'a'}}Copy the code