TS类型系统相关内容详解

2,217 阅读4分钟

名义上的类型别名,类型结构兼容的预防,函数参数的双向协变、及赋值

1. 什么是名义上的类型别名? (名义上的类型别名不兼容)

使用Type 来为类型创建一个别名从而使用一个间断的名字引用该类型

类型别名实质上与原来的类型一样,它仅仅是一个替代的名字,别名只是对类型的一个引用,并创建一个新的类型。类型别名可以让代码的可读性更高

let a: string = 'a'
type aaa = string
let b: aaa = 'b'
 // type bbb extends aaa error  
 console.log(b)

 // 类型别名不能被 extends 和implements

类型结构兼容的预防

基本类型

let str: string = 'Hello'

str = 123 // error TS2322: Type '123' is not assignable to type 'string'.

TS的结构类型兼容

TypeScript里的类型兼容性是基于结构子类型的,这是有别于传统的JAVA、C#基于名义类型的语言。

interface Named {
    name: string
}

class Person {
    name: string
}

let p: Named
p = new Person()

x要兼容y,那么y至少具有与x相同的属性

interface Named {
    name: string
}

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

如何避免

添加差异性,如添加一个私有属性

interface ScreenCoordinate {
  _screenCoordBrand: any
  x: number
  y: number
}
interface PrintCoordinate {
  _printCoordBrand: any
  x: number
  y: number
}

function sendToPrinter(pt: PrintCoordinate) {
}

function getCursorPos(): ScreenCoordinate {
  return <ScreenCoordinate>{ x: 0, y: 0 }
}

// getCursorPos的return值继承了ScreenCoordinate类的私有变量

sendToPrinter(getCursorPos())

PrintCoordinate要兼容ScreenCoordinate,那至少ScreenCoordinate要具有与PrintCoordinate相同的属性

显然,以上案列是错误的

// 正确写法
function sendToPrinter(pt: PrintCoordinate | ScreenCoordinate) {
}

function getCursorPos(): ScreenCoordinate {
  return <ScreenCoordinate>{ x: 0, y: 0 }
}

sendToPrinter(getCursorPos())

如何将更少参数的函数能够赋值给具有更多参数的函数?(函数参数的类型结构兼容)

function handler(arg: string) {
  // ....
}

function doSomething(callback: (arg1: string, arg2: number) => void) {
  callback('hello', 42);
}

// Expected error because 'doSomething' wants a callback of
// 2 parameters, but 'handler' only accepts 1
doSomething(handler);

doSomething的回调函数,必需两个参数 handler只有一个参数

那如何让有更少参数的函数赋值给具有更多参数的函数

function handler(arg: string) {
  // ....
}

function doSomething(callback: (arg1: string, arg2?: number) => void) {
  callback('hello', 42);
}

doSomething(handler);

此时,arg2是一个可有可没有的参数,不会强制执行

跟forEach(callback(element, index, array))中的回调函数可选参数含义的区别

let items = [1, 2, 3];
items.forEach(arg => console.log(arg));

forEach循环数组,必定会产生三个参数传入callback,只是callback接受参数的个数是可选的

mozilla的forEach源码

function ArrayForEach(callbackfn/*, thisArg*/) {
  var O = ToObject(this);
  var len = ToLength(O.length);
 if (arguments.length === 0)
   ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.forEach");
  if (!IsCallable(callbackfn))
   ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn));
  var T = arguments.length > 1 ? arguments[1] : void 0;

  for (var k = 0; k < len; k++) {
   if (k in O) {
   callContentFunction(callbackfn, T, O[k], k, O);
   }
  }
  return void 0;
}

从源码可以看出,callContentFunction(callbackfn, T, O[k], k, O); 并没有对forEach内的回调函数callback的参数做必需使用做判断, 但确实是给回调函数提供了三个参数

因此

[1, 2, 3].forEach(() => console.log('just counting'));
[1, 2, 3].forEach(x => console.log('just counting'));

以上两行代码没有本质区别

为什么函数参数是双向协变?(函数参数的类型)

  • 换个角度说,协变保持了类型参数的继承关系

  • TypeScript 2.6新增了编译器选项--strictFunctionTypes,对函数参数进行严格逆变比较。

  • 首先考虑数组类型构造器: 从Animal类型,可以得到Animal[](“animal数组”)。 是否可以把它当作

协变:一个Cat[] 也是一个Animal[] 逆变:一个Animal[] 也是一个Cat[] 以上二者均不是(不变)? 如果要避免类型错误,且数组支持对其元素的读、写操作,那么只有第3个选择是安全的。 Animal[] 并不是总能当作Cat[], 因为当一个客户读取数组并期望得到一个Cat,但Animal[]中包含的可能是个Dog。所以逆变规则是不安全的。

反之,一个Cat[] 也不能被当作一个Animal[]。 因为总是可以把一个Dog放到Animal[]中。 在协变数组,这就不能保证是安全的,因为背后的存储可以实际是Cat[]。因此协变规则也不是安全的—数组构造器应该是不变。

举个🌰

class Animal {
  public shout() {
    console.log('animal can shout')
  }
}

class Dog implements Animal {
  public eat() {
    console.log('dog eat bone bone')
  }

  public shout() {
    console.log('dog shout wang wang')
  }
}

let animalShout = (animal: Animal) => { console.log(animal.shout()) }

let dogShout = (dog: Dog) => { console.log(dog.shout()) }

dogShout = animalShout
animalShout = dogShout

let funA = (arg: number | string): void => {}
let funB = (arg: number): void => {}
funA = funB
funB = funA

编译结果

$ tsc --strictFunctionTypes Bivariance.ts

Bivariance.ts:26:1 - error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.
  Types of parameters 'dog' and 'animal' are incompatible.
    Property 'eat' is missing in type 'Animal' but required in type 'Dog'.

26 animalShout = dogShout
   ~~~~~~~~~~~

  Bivariance.ts:12:10
    12   public eat() {
                ~~~
    'eat' is declared here.

Bivariance.ts:30:1 - error TS2322: Type '(arg: number) => void' is not assignable to type '(arg: string | number) => void'.
  Types of parameters 'arg' and 'arg' are incompatible.
    Type 'string | number' is not assignable to type 'number'.
      Type 'string' is not assignable to type 'number'.

30 funA = funB
   ~~~~


Found 2 errors.