TypeScript 类型兼容

168 阅读5分钟

我正在参加「掘金·启航计划」

为什么要有类型兼容

因为实际工作中,往往无法类型一致。

const config = {
  a: 1, b: 2, c: 3, d: 4
} 
// 如果类型只接受 3 个 key, 那么 d 就是多余了,传过去的话会导致类型不匹配,就会报错
// 于是 使用 lodash.pick
const newConfig = lodash.pick(config, ['a', 'b', 'c']) // 从 4 个 key 里拿到 3 个 key
runTask(newConfig) // runTask 只接受传的 3 个 key 的对象
// 实际上我们不会这么写
// 大家的习惯都是直接把 4 个选项硬塞给你,为了更好的推广 TS 
// 于是当只需要 3 个属性时,传 4 个也兼容
const config = {
  a: 1, b: 2, c: 3, d: 4
}
runTask(config)

简单类型和普通对象如何兼容

你有的,我都有,则我能代替

变量 y 有的,x 都有,则 x 兼容 y

简单类型

type A = string | number
// 从集合的角度,如果你给我传一个范围更小的,实际上是可以兼容范围更大的
let a: A = 'hi'

当你需要一个父级的时候,我用一个子集代替你

普通对象的兼容

// user 兼容 p, 故将 user 赋值给 p 不报错
// user 小, Person 大
type Person = {
  name: string;
  age: number;
}

// 属性多就是限制条件多【
let user = {
  name: 'hone',
  age: 18,
  id: 1,
  email: 'xxx@gmail.com'
}

let p: Person
// 限制多的,赋值给限制小的
p = user
let user = {
  name: '1',
  age: 18,
  id: 1,
  email: 'xxx'
}

// 类型获取
// 这里的 typeof 是 TS 的,type 开头的是 TS 代码,最终这句话会被擦除,这个 typeof 也会被擦除
type User = typeof user  
// 可以先写对象,再去获取它的类型

Screen Shot 2022-10-06 at 9.21.41 PM.png

type Person = {
  name: string
  age: number
}

type User = {
  name: string,
  age: number,
  id: number,
  email: string
}

let p:Person = {
  name: 'hone', age: 18
}

// 缺少属性 无法赋值
let u: User = p

Screen Shot 2022-10-06 at 9.32.37 PM.png

user 作为参数也不报错

const f1 = (p: Person) => {
  console.log(p)
}
f1(user)

接口如何兼容

父子接口

interface 父接口 {
  x: string
}

interface 子接口 extends 父接口 {
  y: string
}

let objectChiild: 子接口 = {
  x: 'yes',
  y: 'yes'
}

let objectParent: 父接口

objecParent = 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 接受两个参数的函数 = (x: number, y: string) => {
  console.log(x, y)
}

接受两个参数的函数 = 接受一个参数的函数 // OK
接受一个参数的函数 = 接受两个参数的函数 // 报错

// 为什么容忍参数变少 ???

在 JS 中,少写参数是很常见的

const button = document.getElementById(sumbit)
const fn = (e: MouseEvent) => console.log(e)

button.addEventListener('click', fn)
button.addEventListener('click', fn, false)
button.addEventListener('click', fn, true)

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)

mouseListener = listener
listener = mouseListener // 报错

对参数要求多 <= 对参数要求少,对参数要求少的赋值给参数要求多的

interface MyEvent {
  timestamp: number
}

interface MyMouseEvent extends MyEvent {
  x: number;
  y: number;
}

function listenEvent(eventType: string, fn: (n: MyEvent) => void) {
  /*...*/
}

// 我们希望这样用
listenEvent('click', (e: MyMouseEvent) => console.log(e.x + "," + e.y))

// 但只能这样用
listenEvent('click', (e: MyEvent) => {
//const x = (e as number)  // 这样就会报错
  const x = (e as unknown as number) // 这样就不会报错,因为 TS 就是想警告你 ,如果想要逃脱检查,就要写一个很长很长的代码
  // 主观的告诉 TS 这里就不用去报错了,每一次用 as 的时候,如果出了问题就自己负责
  console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
})

// 还可以这么用
listentEvent('click', ((e: MyMouseEvent) => 
  // 也可以把整个函数 as
  cosole.log(e.x + "," + e.y)) as (e: MyEvent) => void)
  
// 这个就太离谱了
// 这里是不可以传 number 的
listenEvent('click', (e: number) => console.log(e)) // 我虽然可以让你随便断言,但是也不可以这样瞎断言

TS 经常要做一个折中,到底要不要严格,如果太严格了会不会写代码写不出来,于是就松散一点。

在项目中有一个 tsconfig.json 文件

{
  "compilerOptions": {
    "strictFunctionTypes":  false  // 把这个关了,就可以单独对某一个类型不检查
  }
}

加了以上代码后, listenEvent('click', (e: MyMouseEvent) => console.log(e.x + "," + e.y)) 中的 MyMouseevent 就不会报错。

如果还觉得的严格,可以写 const x = (e as unknown as number)

赋值表

返回值类型不同能兼容吗 ?

let 返回值属性少集合大 = () => {
  return { name: "Alice" }
}

let 返回值属性多集合小 = () => {
  return { name: "Alice", location: "Seattle" }
}

返回值属性少集合大 = 返回值属性多集合小 // OK
返回值属性多集合小 = 返回值属性少集合大 // 报错:"location" is missing

大 <= 小

限制少 <= 限制多

特殊类型

any -> 表示 any 可以赋值给

Screen Shot 2022-10-07 at 8.58.08 PM.png

红线圈起来的:确定需要单独记忆的。

黄线圈起来的:需要根据配置来定的,在 tsconfig.json 中 写 strictNullChecks: false 进行松散类型检查。

灰色涂起来的根据理论分析就可以得出。

Top Type 与 Bottom Type

top type:    unknown // 顶类型

             string|number  {} object

                            {name}

             string undefined null number {name, age}

bottom type: never  // 底类型 下面的赋值给上面的

any 是一个错误关闭器,除了 any 不能 赋值给 never

也有认为 TS 的 top type 是 unknown 和 any,文章链接, 都可以的,自己能理解的通就行。

一些思考

let b: any = await axios(...)
// any 永远都不需要去断言
b.xxx()
c.yyy()

let a = b // a 也变成了 any

// 当你在项目里用了一个 any, 它会生出一窝的 any,一窝又一窝。

应该用什么?

// 使用 unknown
let b: unknown = await axios(...)

// 当你用 unknown 的时候,没有做类型收窄,你会发现你什么方法都用不了
// unknown 会提醒你去看一下类型,不要随便调用
// unknown 你在用的时候你每一次都要去重新断言
(b as {name: string, id: string}).toFixed()
// 树形结构 所有 JSON 的数据类型
type JSONValue = 
  | string 
  | number
  | null 
  | boolean
  | JSONValue[] 
  | { [k: string]: JSONValue }