TS型变与对象类型进阶

 子类型:给定两个类型A和B,假设B是A的子类型,那么在需要A的地方都可以放心使用B。计作 A <: B (A是B的子类型)。

超类型正好与子类型相反。A >: B (A是B的超类型)。

1 TS 类型

可赋值性指在判断需要B类型的地方是否可以使用A类型。当A <: B 时,是满足可赋值性的,不过TS有两处例外的地方:

1)当A是any时,可赋值给B类型。

function testFun(b:boolean) {
    console.log(b); // 2
}
let a:any = 2;
testFun(a);

TS 这样做是为了方便与JS代码互操作。

2)B 是枚举类型,且至少有一个成员是number类型,而且A是数字。

这是安全性的一大隐患,尽量不要这么操作。

1.1 型变

如果想要保证A可赋值给B对象,那么A对象的每个属性都必须<: B 对象对应的属性。此时,我们说TS对结构(对象和类)的属性类型进行了协变。

type User = {
    name: string,
    money?: number
}
type VipUser = {
    name: string,
    money: number
}

let user: User = {
    name: "牛",
}
let vipUser:VipUser = {
    name: "张",
    money: 1000
}

user = vipUser;
vipUser = user; //ERROR Type User is not assignable to type VipUser

不变

只能是T

逆变

可以是 >: T

协变

可以是 <: T

双变

可以是 <: T或 >: T

表 型变的四种方式

在TS中,每个复杂类型的成员都会进行协变,包括函数的返回类型。不过有个例外:函数的参数类型进行逆变。

1.1.1 函数的型变

如果函数A的参数数量小等于函数B的参数数量,且同时满足下述条件,那么函数A是函数B的子类型:

1)函数A的this类型未指定,或者>:函数B的this类型。

2)函数A的各个参数的类型 >: 函数B对应参数。

3)函数A的返回类型<: 函数B的返回类型。

class Person {
    constructor(public sex: number) {
    }
}
class Man extends Person{
    constructor(public name: string,sex: number) {
        super(sex);
    }
}

type FunA = (this: Person) => void; // 子类型
type FunB = (this: Man) => void; // 父类型

let person:Person = new Person(1);
let man:Man = new Man("李",1);

let funA:FunA = function () {console.log(this);}
let funB:FunB = function () {console.log(this);}

funA.bind(person)();
//funB.bind(person); // ERROR

funA.bind(man)();
funB.bind(man)();

funB = funA;
// funA = funB; // ERROR

函数的协变似乎有些特殊,在我们常规认知中,子类型的作用范围窄于父类型。但是在上面例子中,作为子类型的A函数类型,比父类B可绑定的对象类型更广。子类的唯一定义是:在需要父类的地方可以放心使用子类。上面代码在使用B的地方,可以放心使用A,而使用A的地方不能放心使用B。

type FunA = (this:any) => void; // 子类
type FunB = (this:Man) => void; // 父类

let funA:FunA = function () {console.log(this);};
let funB:FunB = function () {console.log(this);};

funA.bind(man)();
funB.bind(man)();

// funB.bind(person)(); // ERROR
funA.bind(person)();

let fun1: FunA = funB;
let fun2: FunB = funA;

上面代码展示了一个特殊情况:当this为any类型(或未指定时)。这里符合第一个条件。

type FunA = (person: Person) => void; // 子类
type FunB = (man: Man) => void; // 父类

let funA:FunA = function(person:Person) {console.log("funA:" + person.sex);}
let funB:FunB = function (man:Man) {console.log("funB:" + man.name);}

function testFunA(fun: (person: Person) => void) {}
function testFunB(fun: (man: Man) => void) {
    let man = new Man("张",1);
    fun(man);
}

testFunA(funA);
// testFun(funB); // ERROR
testFunB(funA);
testFunB(funB);

1.2 类型扩展

一般来说,TS在推导类型时会放宽要求,故意推导出一个更宽泛的类型。声明变量时如果允许以后修改变量的值,变量的类型将拓展,从字面量扩大到包含该字面量的基类型:

let a = “x”; // string

let b = 1; // number

let c = {x: 1}; // {x: number}

声明为不可变的变量时:

const a = “x”; // “x”

可以通过显示注解类型,防止类型被扩展:

let a: “x” = “x”; // “x”

如果使用let或var 重新为不可变的变量赋值,将自动扩展:

const a = “x”; // “x”

let b = a; // string

const 会禁止类型拓宽,还递归把成员设为readonly:

let x = {a: “t”,b: {x: 12}} as const; // {readonly a: “t”,readonly b: {readonly x:12}}

1.2.1 多余属性检查

TS尝试把新鲜对象字面量类型T赋值给另一个类型U,如果T有U不存在的属性,则报错。

新鲜对象:TS直接从字面量推导出的类型。如果把对象字面量赋值给变量或者对象字面量有断言,则其不是新鲜对象,而是常规对象。

type ObjType = {
    name: string
}

let obj1:ObjType = {
    name: ""
}
let obj2:ObjType = {
    name: "",
    type: 1 // 报错
} // 为新鲜对象
let obj3:ObjType = {
    name: "",
    type: 1
} as ObjType // true 有类型断言

function fun(obj: ObjType) {}
let obj4 = {
    name: "",
    type: 1
}
fun(obj4); // true 已被赋值给变量,为常规对象
fun({
    name: "",
    type: 1 // 报错
}) // 为新鲜变量

2 对象类型进阶

2.1 类型运算符

我们可以像获取对象属性值一样获取类型的某个属性类型 (必须用方括号,不能用点号):

type ObjType = {
    name: string,
    user: {
        type: number,
        name: string
    }
};
type UserType = ObjType["user"]; // {type: number,name:string}

keyof 运算符获取对象所有键的类型,合并为一个字符串字面量类型,以上面的ObjType为例:

type ObjTypeKeysType = keyof ObjType; // "name" | "user"
type UserTypeKeysType = keyof UserType; // "type" | "name"

2.2 映射类型

TS中,我们可以为类型定义索引签名[key:T] : U,该类型的对象可能有更多的键,键的类型为T,值的类型为U。其中T必须为number或string。

而映射类型则在索引签名的基础上,为key做了限制。这让类型更安全:

type IndexType = {
    [K: string]: string
} // 只限定了key 为 string类型

type MappedType = {
    [K in "key1" | "key2"]: string
} // 限定了属性必须有且只有“key1”和“key2”

let indexType: IndexType = {
    "a": "",
    "b": ""
}; //
let mappedType: MappedType = {
    "key1": "",
    "key2": ""
}
let mappedType2:MappedType = {
    "key1": ""
} // 报错 Property key2 is missing in type { key1: string; } but required in type MappedType

2.2.1 Record类型

TS内置的Record类型用于描述有映射关系的对象。是使用映射类型实现的。

type RecordType = Record<"a"|"b",1 | 2 | 3>

let r1: RecordType = {
    "a": 1,
    "b": 1,
}
let r2: RecordType = {
    "a": 1,
    "b": 5
} // 报错 Type 5 is not assignable to type 1 | 2 | 3
let r3: RecordType = {
    "a": 1
} // 报错 Property b is missing in type { a: 1; } but required in type RecordType

2.3 伴生对象模式

TS的类型和值分别在不同的命名空间。在同一作用域中,我们可以有同名的类型和值。在语义上归属同一个名称的类型和值放在一起,其次,使用方可以一次性导入二者。

type User = {
    name: string
}
let User = {
    createUser(name:string):User {
        console.log("这是值创建的");
        return {name: name}
    }
}
let user:User = User.createUser("hmf");
  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值