ReactWeb前端之路让前端飞

Typescript基础5--类型兼容与高级类型

2017-08-27  本文已影响103人  Dabao123

类型兼容

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

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

这样我们定义obj的时候就只能并且必须使obj有一个name属性。假如,在赋值时多加了一个height属性:

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

则typescript会报错

Paste_Image.png

所以,对obj的赋值必须是

obj = {
    name : 'cxh',
}

但是,如果,我们只是定义了这种类型,并没有赋值给obj,不过在以后的运算里难免会有重新对obj赋值,可是我们不能保证对obj赋值的对象是不是符合Name,怎么办?其实typescipt自带类型兼容,它会检查赋值的变量(比如dabao)是否包含name属性,如果包含name则obj兼容dabao.例如:

interface Name {
    name : string
}
let obj : Name;
let dabao = {
    name : 'cxh',
    height : 180
}
obj = dabao;

虽然,dabao是有height属性,但是因为dabao含有name属性,所以根据TypeScript结构化类型系统的基本规则,obj兼容dabao。
在进行赋值前,编译器会对dabao的属性进行遍历,查看是否含有name,并且name是否是string。符合情况就会赋值。
如果要是反过来呢?

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

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

函数兼容

函数的兼容和普通类型或者对象是有差异的。下面我们从函数的参数,以及返回类型进行分析:

let dabao : { (name:string, age : number ,height : number) : object }
let xiaobao : {(name : string, height:number) : object}

xiaobao = function (name : string, height : number) {
    return {
        name : name
    }
}
dabao = xiaobao

对于函数的参数,和对象是有差别的,编译器会遍历xiaobao的参数,如果xiaobao的每个参数类型,能在dabao里面找到,并且顺序一致,就可以赋值。( 注意的是参数的名字相同与否无所谓,只看它们的类型。)差距在于:
对于对象: 要检查 赋值者 包含 被赋值者。
对于函数参数:要检查 赋值者 被 被赋值者 包含。
如果顺序不一致也会报错。

Paste_Image.png
返回值检查
let dabao : { (name:string, height : number) : {name : string} }
let xiaobao : {(name : string, height:number) : {name : string; height:number}}
xiaobao = function (name:string ,height:number) {
    return {
        name : name,
        height : height
    }
}
dabao = xiaobao

编译器对xiaobao的返回值进行检查,如果xiaobao的返回值包含dabao的返回值,则可以复制。
函数的返回值和对象一样。

其实这很好理解,如果我们定义了一个对象必须要用到name属性,在赋值的时候即使它多出了很多属性,也无所谓,大不了我们不用。但是少了我们需要的属性,那就用不了了。
但是函数的参数刚好相反,函数是可以少传或者不传参数,但是多传就会报错。


Paste_Image.png
class dabao {
    name : string
    age : number
    constructor(name: string,age: number){

    }
}
class xiaobao {
    name : string
    constructor(name : boolean){

    }
}
let d : dabao;
let x : xiaobao
x = d 

d如果包含x的属性,就可以赋值。类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

class dabao {
    name : string
    age : number
    constructor(name: string,age: number){

    }
}
class xiaobao {
    name : string
    static height : number
    constructor(name : boolean){

    }
}
let d : dabao;
let x : xiaobao
x = d 

私有成员
私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

class dabao{
    name : string
    age : number
    private height : number
    constructor(name: string,age: number){
    }
}
class xiaobao{
    name : string
    constructor(name : boolean){
    
    }
}
let d : dabao;
let x : xiaobao
x = d 

如果这样:

class dabao {
    name : string
    age : number
    private height : number
    constructor(name: string,age: number){

    }
}
class xiaobao {
    name : string
    private height : number
    constructor(name : boolean){

    }
}

let d : dabao;
let x : xiaobao
x = d 
Paste_Image.png

这样是不可以的,不过如果是protected我们可以这样解决

class obj {
    protected height : number
}
class dabao extends obj{
    name : string
    age : number
    constructor(name: string,age: number){
        super()
    }
}
class xiaobao extends obj{
    name : string
    constructor(name : boolean){
        super()
    }
}

let d : dabao;
let x : xiaobao
x = d 
泛型

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

let dabao = function<T>(x: T): T {
    return x
}

let xiaobao = function<U>(y: U): U {
    return y
}

dabao = xiaobao;

这样也是可以的

let dabao = function<T>(x: T): T {
    return x
}

let xiaobao = function(y:string){
    return y
}
dabao = xiaobao;

但是如果这样

interface dabao<T> {
    name : T
}
let a : dabao<string>

let b : dabao<number>
a = b

就会报错 因为在b里面虽然有name 但是类型不一样。所以编译器会报错。

高级类型

交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。例如:

interface dabao {
    name : string;
    age : number;
}
interface xiaobao {
    age : number;
    height : number;
}
let func : <T,U>(name:T,age:U) => T  = function<T,U>(name:T,age:U) : T&U{
    return Object.assign(name,age)
}
console.log(func<dabao,xiaobao>({name:'cxh',age:12},{age:123,height:180}))

结果:

Paste_Image.png
联合类型
有下面一段代码
function dabao(name: string, age : any) {
    if (typeof age == 'string') {
        return age + 1;
    }else if(typeof age == 'number') {
        return age; 
    }
}

假如我们调用dabao('cxh',true);age既不是string和number类型,而且typescript不会报错。typescript提供了联合类型

function dabao(name: string, age : string | number) {
    if (typeof age == 'string') {
        return age + 1;
    }else if(typeof age == 'number') {
        return age; 
    }
}
Paste_Image.png
interface dabao {
    name : string;
    width:number;
}

interface xiaobao {
    name : string;
    age: number;
    height: number;
}
let d : dabao | xiaobao;
d = {
    name : 'cxh',
    age : 123,
    height : 123,
    width : 123
}

从上面这段代码我们可以看见实际上是没有报错的,d既可以是dabao类型,也可以是xiaobao类型,也可以是dabao加上xiaobao如上所示。但是:

Paste_Image.png

当我们调用d的属性时,发现只有name可以用。
由此我们可以得出交叉类型在定义的时候必须要结合两者的属性:

interface dabao {
    name : string;
    width:number;
    set(): void;
}

interface xiaobao {
    name : string;
    age: number;
    height: number;
}
let d : dabao & xiaobao;
d = {
    name : 'cxh',
    width : 12,
    set : () => {},
    age : 12,
    height: 180
}
console.log(d.name)
console.log(d.age)
console.log(d.height)
console.log(d.width)

即使少一个属性也会报错:

Paste_Image.png

结合类型在定义的时候至少包含其中一种类型:

interface dabao {
    name : string;
    width:number;
    set(): void;
}

interface xiaobao {
    name : string;
    age: number;
    height: number;
}
let d : dabao | xiaobao;
d = {
    name : 'cxh',
    width : 12,
    set : () => {},
}

或者

d = {
    name : 'cxh',
    width : 12,
    set : () => {},
    age: 13
}

再或者

d = {
    name : 'cxh',
    width : 12,
    set : () => {},
    age: 13,
    height: 180
}

都不会报错,但是无论上述哪种能生效的只有共有的属性;

Paste_Image.png

typescript在定义类型的时候d既可以是dabao类型也是xiaobao类型,但是typescript并不能确定用户到底要传哪一个,所以对于这两种类型单独存在的成员不能保证到底能不能有。所以索性只去识别公共的部分。
但是,如果我们非得需要去识别呢?
第一种方式就是使用类型断言

console.log((<dabao>d).name)
console.log((<dabao>d).width)
console.log((<xiaobao>d).age)
console.log((<xiaobao>d).height)

第二种:用户自定义的类型保护

用户自定义的类型保护
function isDabao(d : dabao | xiaobao) : d is dabao{
    return (<dabao>d).width !== undefined;
}

调用函数isDabao 检查d是否为dabao 是即返回true。


interface dabao {
    name : string;
    width:number;
    set(): void;
}

interface xiaobao {
    name : string;
    age: number;
    height: number;
}
let d : dabao | xiaobao;
d = {
    name : 'cxh',
    width : 12,
    set : () => {},
    age: 13,
    height: 180
}
function isDabao(d : dabao | xiaobao) : d is dabao{
    return (<dabao>d).width !== undefined;
}
if(isDabao(d)){
    console.log(d.width)
}else {
    console.log(d.height)
}

typeof类型保护

function isNumber(x : any) :x is number{
    return typeof x === 'number'
}

instanceof类型保护

class dabao {
    name : string = 'cxh';
    height : number = 180
}
class xiaobao {
    name : string = 'yy'
    age : number = 26 
}

let d : dabao | xiaobao;
d = new dabao;
if (d instanceof dabao) {
   console.log(d.height) ;
}
if (d instanceof xiaobao) {
   console.log(d.age) ;
}

或者

interface obj {
    timer : number
}
class dabao implements obj{
    timer : number
    name : string = 'cxh';
    height : number = 180
}
class xiaobao implements obj{
    timer : number
    name : string = 'yy'
    age : number = 26 
}

let d : obj;
function getRandom() {
    return Math.random() < 0.5 ?
        new dabao() :
        new xiaobao();
}
d = getRandom();
if (d instanceof dabao) {
   console.log(d.height) ;
}
if (d instanceof xiaobao) {
   console.log(d.age) ; 
}
类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
   if (typeof n === 'string') {
       return n;
   }
   else {
       return n();
   }
}
let a = getName('cxh');
let b = getName(() => {
   return 'dabao'
})

console.log(a)
console.log(b)
Paste_Image.png

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

type Container<T> = { value: T };

我们也可以使用类型别名来在属性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}
let t : Tree<string>
t.value = 'cxh';
t.left.value = 'ss'
t.left.left.value = 'ss'

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
接口 vs. 类型别名

像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。

其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是Interface,但悬停在aliased上时,显示的却是对象字面量类型。

Paste_Image.png Paste_Image.png

另一个重要区别是类型别名不能被extends和implements(自己也不能extends和implements其它类型)。

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
       }else  if (easing === "ease-out") {
       }else {
       }
}
可辨识联合(Discriminated Unions)

你可以合并字符串字面量类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
具有普通的字符串字面量属性—可辨识的特征。
一个类型别名包含了那些类型的联合—联合。
此属性上的类型保护。

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}
完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle到Shape,我们同时还需要更新area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

有两种方式可以实现。 首先是启用 --strictNullChecks并且指定一个返回值类型:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

因为switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回undefined。 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为number | undefined。 然而,这种方法存在些微妙之处且 --strictNullChecks对旧代码支持不好。
第二种方法使用never类型,编译器用它来进行完整性检查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

这里,assertNever检查s是否为never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

上一篇 下一篇

猜你喜欢

热点阅读