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

类型兼容

如果,我们定义一个包含属性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的时候也更加明显。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容

  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,794评论 1 10
  • { "Unterminated string literal.": "未终止的字符串文本。", "Identifi...
    一粒沙随风飘摇阅读 10,551评论 0 3
  • 越听越孤独不是因为你在听的歌,是因为你的孤独让你感到孤独
    AggiePanda阅读 178评论 0 0
  • 这是我第一次听到他的声音,那时候我在上班,因为妈妈提前把相亲对象手机号码告诉了我,我将其存了起来,“***”三个字...
    小颖sss阅读 233评论 0 0
  • 海头镇 户外行走的最后一站,是连云港的海头镇,来这里是想给家人带些海鲜。 来来往往中,我发现我特别喜欢一些朴实的小...
    风又飘飘阅读 231评论 0 0