基础前端node

TypeScript 混入、类型推断、交叉类型、联合类型、声明合

2020-02-05  本文已影响0人  CondorHero
一、混入(mixins)

mixins 混入可以理解为扩展,把本来不属于自己的东西硬生生的给弄到自己身上,在 TypeScript 中主要分为类的混入和对象的混入。

mixins 与 extends 如何区分?
继承是上下级关系,混入是平级关系,例如人和狗都属于(继承)动物类,但是人和狗都有吃这个方法,如果这个方法你在人里面定义了,就可以混入到狗里面。

1.1 对象的混入 mixins

JavaScript 对象的混入,把 b 的内容混入 a 并赋值给 c ,a 得到扩展有 b 的属性和方法。

var a = {name:"name"};
var b = {age:18};
var c = Object.assign(a,b);
console.log(a,b,c);
//{name: "name", age: 18} {age: 18} {name: "name", age: 18}

上面的混入我们扩展了 a ,但是大多数情况下我们不会去直接扩展 a ,所以上面的混入我们都是这样写。

var c = Object.assign({},a,b);

TypeScript 中的混入:

interface ObjectA{
    a:string;
}
interface ObjectB{
    b:number;
}
var a:ObjectA =  {a:"a"};
var b:ObjectB =  {b:12};
var c = Object.assign(a , b);//c的类型交叉类型为ObjectA & ObjectB

TypeScript 混入之后得到的值根据类型推论交叉类型

1.2 类的混入

ES5 的混入:

function A() {};
A.prototype.eat = function(){};
function B() {};
A.prototype.run = function(){};
const mixins = (target,from)=>{
    Object.keys(from).forEach(key=>{
        console.log(from[key],target[key]);
        target[key] = from[key];
    });
};
mixins(A.prototype , B.prototype);
console.log(A.prototype);
//{eat: ƒ, run: ƒ, constructor: ƒ}

ES6 的混入

class A {
    eat(){}
}
class B {
    run(){}
}
const mixins = (target,from)=>{
    Object.getOwnPropertyNames(from).forEach(key=>{
        console.log(from[key],target[key]);
        target[key] = from[key];
    });
};
mixins(A.prototype , B.prototype);
console.log(A.prototype);
//{eat: ƒ, run: ƒ, constructor: ƒ}

ES5 之所以和 ES6 混入写法不同在于,ES6 类的内部所有定义的方法,都是不可枚举的(non-enumerable),但是 ES5 是可以的。所以:

Object.getOwnPropertyNames 和 Object.keys 的区别?
即 Object.keys 只适用于可枚举的属性,而 Object.getOwnPropertyNames 返回对象自动的全部属性名称。

TypeScript 版混入:

class A {
    public isA: boolean;
    public funA() { }

}


class B {
    public isB: boolean;
    public funB() { }
}

class AB implements A , B {
    public isA: boolean = false;
    public isB: boolean = false;
    public funA: () => void;
    public funB: () => void;
}
applyMixins(AB, [A, B]);


function applyMixins(base: any, from: any[]) {
    from.forEach(fromItem => {
        Object.getOwnPropertyNames(fromItem.prototype).forEach(key => {
            Object.defineProperty(base.prototype, key, Object.getOwnPropertyDescriptor(fromItem.prototype , key ));
        });
    });
}
console.log(AB.prototype);

Object.getOwnPropertyDescriptor(obj,key);的作用就是能够获取对象的key描述可以完整作为 Object.defineProperty方法的第三个参数。

var obj = { "name":"Condor Hero" };
var desc = Object.getOwnPropertyDescriptor (obj, "name" );
console.log(desc);
{
    value: 'Condor Hero',
    writable: true,
    enumerable: true,
    configurable: true
}

其实类的混入最简单的写法是:

Object.assign(A.prototype , B.prototype);
二、交叉类型(Intersection Types)和联合类型(Union Types)
2.1 交叉类型(Intersection Types)是将多个类型合并为一个类型。

这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable同时是Person和Serializable和Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子("target": "es5"):

function extend<First, Second>(first: First, second: Second): First & Second {
    const result: Partial<First & Second> = {};
    for (const prop in first) {
        if (first.hasOwnProperty(prop)) {
            (<First>result)[prop] = first[prop];
        }
    }
    for (const prop in second) {
        if (second.hasOwnProperty(prop)) {
            (<Second>result)[prop] = second[prop];
        }
    }
    return <First & Second>result;
}

class Person {
    constructor(public name: string) { }
}

interface Loggable {
    log(name: string): void;
}

class ConsoleLogger implements Loggable {
    log(name) {
        console.log(`Hello, I'm ${name}.`);
    }
}

const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);

这个代码没看懂。。。

2.2 联合类型(Union Types)

一个变量希望传入 number 或 string 类型的参数,首先使用联合类型。 联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以 number | string | boolean 表示一个值可以是 number,string,或 boolean。

var a = number | string | boolean;
三、类型推论
3.1 基础

TypeScript 中没有明确指定类型的地方,类型推论会尝试给出类型,比如:

const x = 3;

此时,类型推论会发生在初始化变量和成员的时候、设置默认参数值和决定函数的返回值的时候,变量 x 类型被推断为 number

3.2 最佳通用类型

当需要从几个表达式中推断类型的时候,会使用这些表达式的类型来推断出一个最合适的通用类型,比如:

let x = [0, 1, null];

此时为了推断 x 的类型,需要遍历整个数组的所有元素的类型。从遍历结果看有两个选择,分别是 numbernull计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

因为最终的通用类型也是来自候选类型,有时候候选类型共享相同的通用类型,但是却没有一个类型能够为所有候选类型所兼容,比如:

let zoo = [new Rhino(), new Elephant(), new Snake()];

上面的数组中,如果有个 Animal ,并且是上面三个 class 的基类,则可以作为最佳通用类型。但是没有找到最佳通用类型,类型推断的结果成为联合数组类型:(Rhino | Elephant | Snake)[]

3.3 上下文类型

TypeScript 的类型推断也可能按照相反的方向进行,称为 按上下文归类。按上下文归类会发生在表达式的类型与所处的位置相关时。

window.onmousedown = function(mouseEvent: number) {
    console.log(mouseEvent.button);  //<- Error
};

上面代码中会报错如下:

而此时如果将 mouseEvent 类型声明为 any 则是 OK 的,因为对于 window.onmousedown 根据上下文推断 mouseEvent 参数的类型不可能是 number,如果不声明 mouseEvent 的类型,则会推断成 any ,而此时如果声明是 any 则也不会报错。

window.onmousedown = function(mouseEvent: any) {
    console.log(mouseEvent.button); 
};

如果明确声明了参数类型,则会忽略上下文的类型

上下文类型会在很多情况下使用到。 通常包含函数的参数、赋值表达式的右边、类型断言、对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:

function createZoo(): Animal[] {
    return [new Rhino(), new Elephant(), new Snake()];
}

上面从上下文中推断出了 Animal 是最合适的最佳候选类型。

四、声明合并

在 JavaScript 中,如果使用 var 定义了同名变量,后者会覆盖前者,let 和 const 不允许出现同名变量。在 TypeScript 中,声明合 是将对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。

4.1 同类型的声明合并

假设定义了两个相同名字的函数、接口、命名空间或类,看看这种同类型的声明合并成为一种类型的情况:

  1. 函数的合并
    两个相同名字的函数会发生函数重载,我们可以使用重载定义多个函数类型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}
  1. 接口的合并
    接口中的属性在合并时会简单的合并到一个接口中:
interface  Alarm  {
   price:  number;
}
interface  Alarm  {
   weight:  number;
}

相当于:

interface  Alarm  {
   price:  number;
   weight:  number;
}

注意,合并的属性的类型必须是唯一的

interface  Alarm  {
   price:  number;
}
interface  Alarm  {
   price:  number;  // 虽然重复了,但是类型都是 `number`,所以不会报错
   weight:  number;
}

interface  Alarm  {
   price:  number;
}
interface  Alarm  {
   price:  string;  // 类型不一致,会报错
   weight:  number;
}
// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type.  Variable 'price' must be of type 'number', but here has type 'string'.

接口中方法的合并,与函数的合并一样:

interface  Alarm  {
   price:  number;
   alert(s:  string):  string;
}
interface  Alarm  {
  weight:  number;
  alert(s:  string, n:  number):  string;
}

相当于

interface  Alarm  {
   price:  number;
   weight:  number;
   alert(s:  string):  string;
   alert(s:  string, n:  number):  string;
}
  1. 类的合并
    类的合并与接口的合并规则一致。
  2. 命名空间的合并
    命名空间的合并与接口的合并规则一致,命名空间共享出去的是 export 出去的东西。
4.2 不同类型之间的合并
  1. 命名空间和类(函数)
    命名空间和类,类一定的在命名空间前面,合并的最终结果为类(函数)。
class Validations { };
namespace Validations {
    export const a:number = 10;
};
console.log(Validations.a);//10

合并规则是命名空间 export 导出的东西,将会作为类的静态属性。所以命名命名空间可以为类增加静态属性。

  1. 命名空间和枚举
    书写没有顺序要求。
enum Color { blue , green , white};
namespace Color { export const yellow = 99 };
console.log(Color);//{0: "blue", 1: "green", 2: "white", blue: 0, green: 1, white: 2, yellow: 99}
五、类型兼容性

基本没啥用,你要用这个就是自己给自己找麻烦。类型兼容性主要涉及的就是类、函数、接口等等,我们在使用时候的规范,只要正常使用就没什么事情。

5.1 对象兼容

如果,我们定义一个包含属性 name 的对象

interface  Name {
  name : string
}
let obj : Name;

这样我们定义 obj 的时候就只能并且必须使 obj 只有一个 name 属性,不能多也不能少。所以,对 obj 的赋值只能是:

obj = {
    name : 'heshen',
}

但是,如果我们只是定义了这种类型的变量但是并没有赋值,但又保不齐以后会对 obj 赋值,可是我们不能保证对 obj 赋值的对象是不是符合Name,怎么办?其实 TypeScript 是自带类型兼容,它会检查赋值的变量是否包含 name 属性,如果包含 name 属性则 obj 兼容 myname 例如:

interface Name {
    name : string
}
let obj : Name;
let myname = {
    name : 'heshen',
    height : 180
}
obj = myname;

虽然,myname 是有 height 属性,但是也含有 name 属性,所以根据 TypeScript 结构化类型系统的规则,obj 兼容 myname。如果要是反过来是否也成立呢?

interface Name {
    name : string;
    age : number
}
let obj : Name;
let myname = {
    name : 'cxh'
}
obj = myname;

obj 需要 name ,age 两个属性,但是 myname 只有一个 name,编译器遍历 myname,发现并没有找到age,所以肯定报错了。

现在给 interface 自由增加属性又多了一个方法,那就是类型兼容,这是第三个方法了。

上面提到结构类型系统,那什么是结构类型系统,又什么是名义(nominal)类型系统?

interface Named {
    name: string;
}
class Person {
    name: string; 
   // strictPropertyInitialization 检查这里会报错误,因为没有在 constructor 初始化
}
let p: Named = new Person();

// 可行,因为这是一个结构类型

在传统的面向对象的语言中(比如 C# 或者 Java)上面代码会报错误,因为没有明确声明 Person 与 Named 的关系,Person 没有实现 Named 接口,这就是名义结构系统,必须产生关系。结构类型系统表示,只要数据结构相同就正确。

5.2 函数兼容

判断基础类型或者是对象格式的类型还是比较容易判断出来,关键在于如何判断两个函数是兼容的。

let funcA = (a: number) => 0;
let funcB = (b: number, s: string) => 1;

funcB = funcA; // ok

funcA = funcB; // 报错

比较函数主要是比较它们的参数列表能否兼容,形参的名称是不重要的,重要的是顺序和类型。

将 funcA 赋给 funcB 是没问题的, 因为 funcB 的参数足以兼容 funcA 的参数,但是将 funcB 赋给 funcA 是不行的,funcA 无法兼容多的 s 参数。

funcB = funcA 这样的形式是 OK 的,因为 JavaSript 本身就经常忽略额外的参数,但是缺少参数是不行的。

函数的兼容除了比较参数之外,还会比较返回值类型。

let funcA = () => ({name: 'name'});
let funcB = () => ({name: 'name', age: 18});

funcA = funcB; // ok
funcB = funcA; // 报错

上面 funcB=funcA 会报错,因为 funcA 无法兼容 funcB 的返回值中的 age 属性。类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够复制给目标函数或者反过来的时候才能赋值成功。这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却是用了不是那么精确的类型信息。

var funA = function(argv:string | number):void{};
var funB = function(argv:string):void{};

// funA = funB;
funB = funA;

可选参数及剩余参数
比较函数的兼容性的时候,可选参数和必选参数是可以互换的。源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。

当一个函数有 rest 参数时,它被当做无限个可选参数。

这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded。

函数接收一个回调函数,而对于程序员来说是可预知的参数,但对类型系统来说是不确定的参数来调用:

function invokeLater(args: any[], callback:(...args: any[]) => void) {}

invokeLater([1,2], (x,y) => { console.log(x +' '+y) });

invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

函数重载
对于重载的函数,源函数的每个重载都要在目标函数上找到对应的函数名。确保了目标函数可以在所有源函数可调用的地方调用。

5.3 枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

enum Status {Reday, Waiting};
enum Color {Red, Blue, Green};
Status.Waiting = 10;//ok

let s= Status.Ready;
s = Color.Red; // 报错
5.4 类

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

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

类的私有成员和受保护成员?
类的私有成员和受保护成员会影响兼容性。当检查类的实例的兼容的时候,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

5.5 泛型

因为 TypeScript 是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如:

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

x = y; // OK 因为 y 能够匹配 x 的结构

上面代码没有指定具体的接口成员,因此此时 x 和 y 结构类型其实是相同的。

但是如果此时加了一个成员:

interface Empty<T> {
    name: T
}
let x: Empty<number>;
let y: Empty<number>;
 y = x; // 报错

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。

六、类型别名

类型别名用来给一个类型起个新名字。

type name = string;
var a:name = "字符串类型";
上一篇下一篇

猜你喜欢

热点阅读