6TypeScript中的类型兼容与赋值

61 阅读4分钟

一. 为什么要有类型兼容?

  • 实际工作中往往无法做到类型一致
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';

1.jpeg

  • 也就是说,小范围可以赋值给大范围,另外小范围包含于大范围,大范围包含小范围

四. 普通对象的兼容

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;

六. 函数如何兼容

  1. 函数包括参数和返回值,我们先看参数
let 接收一个参数的函数 = (a: number) => {
  console.log(a);
};

let 接收两个参数的函数 = (b: number, s: string) => {
  console.log(s, b);
};

接收两个参数的函数 = 接收一个参数的函数; // ok
接收一个参数的函数 = 接收两个参数的函数; // 报错
  • 通过画图来理解

2.jpeg

  • 如何去理解,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));
  • 结论: 参数少的可以传给参数多的
  1. 函数的参数类型不同能否兼容?
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; // 报错
  • 还是通过画图去理解,注意看箭头颜色,先看等号右边,如果是红色,那么左边就是绿色。反之亦然。

3333.jpeg

  • 结论: 对参数要求少的可以赋值给对参数要求多,实际开发就是看会不会报错

七. 函数参数兼容性的配置

  • "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而有区分

4.png

十. 顶类型和底类型

  • 该部分知识属于类型系统,直接记住结论,底类型是never可以赋值给任何类型,顶类型是unknown可以被任何类型所赋值