一. 为什么要有类型兼容?
- 实际工作中往往无法做到类型一致
const config = {
a: 1,
b: 2,
c: 3,
d: 4,
};
// 如果runTask只接收a,b,c,那么d就多余了,多余的类型传入会导致类型不匹配
// 所以用lodash.pick只接收'a','b','c'
const newConfig = lodash.pick(config, ['a', 'b', 'c']);
runTask(newConfig);
// 上面这种写法 VS 下面这种写法
const config = {
a: 1,
b: 2,
c: 3,
d: 4,
};
// 事实上大多数js开发者都是这么用,多塞一个属性不用也不会有影响
// 所以作为ts应该兼容这种写法,否则ts会很难推广,也就是说当面对只需要传三个属性的对象的场景,多传一个属性也应该兼容
runTask(newConfig);
二. 什么是类型兼容
- 一个通俗的比喻,你有的我都有,则我能代替你。ts中如果一个变量y有的,x都有,则x兼容(代替)y。
三. 简单类型的兼容
// 如何理解'hi' 可以 赋值给A,通过集合的角度,具体见下图
type A = string | number;
let a: A = 'hi';
- 也就是说,小范围可以赋值给大范围,另外小范围包含于大范围,大范围包含小范围
四. 普通对象的兼容
type Person = {
name: string;
age: number;
};
let user = {
name: 'frank',
age: 18,
id: 1,
email: 'mike@126.com',
};
// 获取user的类型
type user1 = typeof user;
let p: Person;
// 属性多的可以赋值给属性少,有两种理解方式,
// 1.属性多意味着范围小(属性多要求多,满足要求的少),范围小的可以赋值给范围大 2.可以理解为之遥满足person的必要属性即可赋值
p = user;
// 如果反过来,不能赋值,报错提示:缺少id, email的属性
let p2: Person = {
name: 'apple',
age: 1,
};
let u2: user1 = p2;
- user作为参数也不报错
type Person = {
name: string;
age: number;
};
let user = {
name: 'frank',
age: 18,
id: 1,
email: 'mike@126.com',
};
// 多传了属性,不用不影响
const f1 = (p: Person) => {
console.log(p);
};
f1(user);
五. 接口的兼容-父子接口
interface 父接口 {
x: string;
}
interface 子接口 extends 父接口 {
y: string;
}
let objectChild: 子接口 = {
x: 'yes',
y: 'yes',
};
let objectParent: 父接口;
// 子接口代替父接口,通常子接口的属性更多,属性多的代替属性少的
objectParent = objectChild;
- 如果两个接口没有关系能否代替?答案是可以。
interface 有左手的人 {
left: string;
}
interface 有双手的人 {
left: string;
right: string;
}
let person: 有双手的人 = {
left: 'yes',
right: 'yes',
};
// 有双手的人代替了有左手的人,这两个类型没有父子继承关系
let personLeft: 有左手的人 = person;
六. 函数如何兼容
- 函数包括参数和返回值,我们先看参数
let 接收一个参数的函数 = (a: number) => {
console.log(a);
};
let 接收两个参数的函数 = (b: number, s: string) => {
console.log(s, b);
};
接收两个参数的函数 = 接收一个参数的函数; // ok
接收一个参数的函数 = 接收两个参数的函数; // 报错
- 通过画图来理解
- 如何去理解,js程序员认为参数少传问题不大
const button = document.getElementById('submit')!;
const fn = (e: MouseEvent) => console.log(e);
// 通常都是这么写,不会去写第三个参数,上面的写法兼容下面两种
button.addEventListener('click', fn);
button.addEventListener('click', fn, false);
button.addEventListener('click', fn, true);
// js中函数少写参数非常常见,下面的写法兼容上面
let items = [1, 2, 3];
items.forEach((item, index, array) => console.log(item));
items.forEach((item) => console.log(item));
- 结论: 参数少的可以传给参数多的
- 函数的参数类型不同能否兼容?
interface MyEvent {
target: string;
}
interface MyMouseEvent extends MyEvent {
x: number;
y: number;
}
let listener = (e: MyEvent) => console.log(e.target);
let mouseListener = (e: MyMouseEvent) => console.log(e.x, e.y);
// 这里要强调一下,这里只是类型兼容不会报错,而非追求功能一致s
mouseListener = listener;
listener = mouseListener; // 报错
- 还是通过画图去理解,注意看箭头颜色,先看等号右边,如果是红色,那么左边就是绿色。反之亦然。
- 结论: 对参数要求少的可以赋值给对参数要求多,实际开发就是看会不会报错
七. 函数参数兼容性的配置
"strictFunctionTypes": false,这样配置就不是非常严格- 在不开启这个宽松模式的情况下,实际开发过程中,当函数参数不匹配,我们用as断言来解决
interface Event {
timestamp: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
function listenEvent(eventType: string, handler: (n: Event) => void) {
/* ... */
}
// 我们希望这样用
listenEvent('click', (e: MyMouseEvent) => console.log(e.x + ',' + e.y));
// 但只能这样用
listenEvent('click', (e: Event) =>
console.log((e as MyMouseEvent).x + ',' + (e as MyMouseEvent).y)
);
// 还可以这么用
listenEvent('click', ((e: MyMouseEvent) => console.log(e.x + ',' + e.y)) as (
e: Event
) => void);
// 这个就太离谱了,还是报错
listenEvent('click', (e: number) => console.log(e));
八. 开始研究函数的返回值
let 返回值属性少集合大 = () => {
return { name: 'Alice' };
};
let 返回值属性多集合小 = () => {
return { name: 'Alice', location: 'Shanghai' };
};
返回值属性少集合大 = 返回值属性多集合小; // ok
返回值属性多集合小 = 返回值属性少集合大; // 会报错
- 结论:小的赋值给大的,限制多的赋值给限制少
九. 特殊类型
- 类型赋值表,左边的类型可以赋值给上边的类型,其中有一些是根据是否严格模式
"strictNullChecks": false而有区分
十. 顶类型和底类型
- 该部分知识属于类型系统,直接记住结论,底类型是never可以赋值给任何类型,顶类型是unknown可以被任何类型所赋值