typescript 定义属性-你不知道的 TypeScript 中间类型

前言

对于有 JavaScript 基础的朋友来说,TypeScript 上手似乎很容易。 您只需要简单掌握其基本类型系统即可逐步将JS应用过渡到TS应用。

// js
const double = (num) => 2 * num
// ts
const double = (num: number): number => 2 * num

然而,当应用程序变得越来越复杂时,我们很容易将一些变量设置为任何类型,TypeScript 写起来就变成了 AnyScript。 为了让您更深入地了解 TypeScript 的类型系统,本文将重点介绍中级类型,以帮助您摆脱 AnyScript。

子类

在解释中间类型之前,我们需要简单了解一下子类是什么。

子类是强类型语言中的一个重要概念。 合理使用类库可以提高代码的复用性,使系统更加灵活。 以下是维基百科对基类的描述:

子类允许程序员在使用强类型编程语言编写代码时使用一些稍后指定的类型,并在实例化时将这些类型指定为参数。

类库是用一对尖括号()来表示的,尖括号内的字符称为类型变量,这个变量就是用来表示类型的。

function copy(arg: T): T {
  if (typeof arg === 'object') {
    return JSON.parse(
      JSON.stringify(arg)
    )
  } else {
    return arg
  }
}

这个类型T在不调用copy函数的时候是不确定的。 只有当调用copy时,我们才知道T具体代表哪些类型。

const str = copy('my name is typescript')

我们在VSCode中可以看到,copy函数的参数和返回值都已经有类型了,也就是说,我们在调用copy函数的时候,给了类型变量T一个字符串参数。 虽然我们在调用copy时可以省略尖括号,但是通过TS的类型推断可以确定T是一个字符串。

中间型

除了字符串、数字、布尔值等基本类型外,我们还应该了解一些类型声明中的一些中间用法。

十字型(&)

简单来说,交叉类型就是将多种类型合并为一种类型。 我个人认为称其为“合并型”更为合理,其句型规则与逻辑“与”符号一致。

T & U

如果我现在有两个类,一个按钮和一个超链接,现在我需要一个带有超链接的按钮,我可以使用十字类型来实现。

interface Button {
  type: string
  text: string
}
interface Link {
  alt: string
  href: string
}
const linkBtn: Button & Link = {
  type: 'danger',
  text: '跳转到百度',
  alt: '跳转到百度',
  href: 'http://www.baidu.com'
}

联合类型 (|)

关节类型的语法规则与逻辑“或”符号一致,表示其类型是关节的多种类型中的任意一种。

T | U

例如,在前面的Button组件中,我们的type属性只能指定几个固定的字符串。

interface Button {
  type: 'default' | 'primary' | 'danger'
  text: string
}
const btn: Button = {
  type: 'primary',
  text: '按钮'
}

类型别名(类型)

如果上面提到的交叉类型和联合类型需要在多个地方使用,就需要通过类型别名的方法为这两种类型声明一个确定的名称。 类型别名与声明变量的句型类似,只需要将 const 和 let 替换为 type 关键字即可。

type Alias = T | U

type InnerType = 'default' | 'primary' | 'danger'
interface Button {
  type: InnerType
  text: string
}
interface Alert {
  type: ButtonType
  text: string
}

类型索引 (keyof)

keyof 与Object.keys类似,用于获取socket中Key的联合类型。

interface Button {
    type: string
    text: string
}
type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"

还是以之前的Button类为例。 Button的type类型来自另一个类ButtonTypes。 按照前面的写法,每次更新ButtonTypes时,都需要改变Button类。 如果我们使用keyof,就不会有这个担心了。

interface ButtonStyle {
    color: string
    background: string
}
interface ButtonTypes {
    default: ButtonStyle
    primary: ButtonStyle
    danger: ButtonStyle
}
interface Button {
    type: 'default' | 'primary' | 'danger'
    text: string
}
// 使用 keyof 后,ButtonTypes修改后,type 类型会自动修改 
interface Button {
    type: keyof ButtonTypes
    text: string
}

类型约束(扩展)

这里的extends关键字和类后面使用extends的继承功能是不同的。 在子类中使用它的主要作用是对基类进行约束。 我们用上面写的copy方法再举一个反例:

type BaseType = string | number | boolean
// 这里表示 copy 的参数
// 只能是字符串、数字、布尔这几种基础类型
function copy(arg: T): T {
  return arg
}

如果我们传入一个对象,就会出现问题。

Extends 通常与 keyof 一起使用。 例如,如果我们有办法获取一个对象的值,而该对象是不确定的,我们可以使用extends和keyof来约束。

function getValue(obj: T, key: K) {
  return obj[key]
}
const obj = { a: 1 }
const a = getValue(obj, 'a')

这里的getValue方法可以根据传入的参数obj来约束key的值。

类型映射(中)

in关键字的作用主要是做类型映射,遍历现有socket的key或者遍历联合类型。 下面以外部类库socket Readonly为例。

type Readonly = {
    readonly [P in keyof T]: T[P];
};
interface Obj {
  a: string
  b: string
}
type ReadOnlyObj = Readonly

我们可以构建这个逻辑。 首先,keyofObj 获取联合类型 'a'|'b'。

interface Obj {
    a: string
    b: string
}
type ObjKeys = 'a' | 'b'
type ReadOnlyObj = {
    readonly [P in ObjKeys]: Obj[P];
}

之后PinObjKeys就相当于执行了一次forEach的逻辑,遍历'a'|'b'

type ReadOnlyObj = {
    readonly a: Obj['a'];
    readonly b: Obj['b'];
}

最后,你可以获得一个新的套接字。

interface ReadOnlyObj {
    readonly a: string;
    readonly b: string;
}

条件类型 (U?X:Y)

条件类型的句子规则与三元表达式一致,常用于一些类型不确定的情况。

T extends U ? X : Y

里面的意思是,如果T是U的子集,则为X类型,否则为Y类型。下面以外部类库socket Extract为例。

type Extract = T extends U ? T : never;

如果T中的类型在U中存在typescript 定义属性,则返回它,否则丢弃它。 假设我们的两个类有3个公共属性,通过Extract就可以提取出这3个公共属性。

interface Worker {
  name: string
  age: number
  email: string
  salary: number
}
interface Student {
  name: string
  age: number
  email: string
  grade: number
}
type CommonKeys = Extract
// 'name' | 'age' | 'email'

工具库

TypesScript 中有很多外部工具库。 上面介绍了Readonly和Extract这两种类型。 外部类库定义在TypeScript的外部lib.es5.d.ts中,因此可以直接使用,无需任何依赖。 正在使用。 我们来看看一些常用的工具库。

部分的

type Partial = {
    [P in keyof T]?: T[P]
}

Partial 用于将套接字的所有属性设置为可选状态。 首先通过keyofT取出类型变量T的所有属性,然后通过in遍历,最后得到一个? 添加在属性之后。

当我们通过TypeScript编写React组件时,如果组件的属性有默认值,我们可以通过Partial将属性值设置为可选。

import React from 'react'
interface ButtonProps {
  type: 'button' | 'submit' | 'reset'
  text: string
  disabled: boolean
  onClick: () => void
}
// 将按钮组件的 props 的属性都改为可选
const render = (props: Partial = {}) => {
  const baseProps = {
    disabled: false,
    type: 'button',
    text: 'Hello World',
    onClick: () => {},
  }
  const options = { ...baseProps, ...props }
  return (
    
  )
}

必需的

type Required = {
    [P in keyof T]-?: T[P]
}

required的作用正好和Partial相反,即将socket中的所有可选属性都改为required。 区别在于替换 ? 部分带有 -?。

记录

type Record = {
    [P in K]: T
}

Record接受两个类型变量,并且Record生成的类型具有类型K中存在的属性,且值为T类型。这里的一个疑问点是给类型K添加一个类型约束,extendskeyofany,我们可以先看看keyofany是什么。

基本上,类型K被约束在string|number|symbol中,而它恰好是对象索引的类型,也就是说,类型K只能被指定为这些类型。

我们经常在业务代码中构造一个对象的字段typescript 定义属性,但是链表不方便索引,所以有时我们会取出一个对象数组作为索引,然后构造一个新的对象。 假设有一个产品列表的链接列表。 为了在商品列表中查找名为“每日坚果”的商品,我们通常通过遍历链表来查找,链表比较长。 为了方便起见,我们将这个字段重写为一个对象。

interface Goods {
  id: string
  name: string
  price: string
  image: string
}
const goodsMap: Record = {}
const goodsList: Goods[] = await fetch('server.com/goods/list')
goodsList.forEach(goods => {
  goodsMap[goods.name] = goods
})

挑选

type Pick = {
    [P in K]: T[P]
}

Pick主要用于提取socket的一些属性。 做过Todo工具的朋友都知道,Todo工具在编辑时只会填写描述信息,在预览时只会填写标题和完成状态,所以我们可以使用Pick工具提取Todo套接字的两个属性并生成一个新的。 TodoPreview 类型。

interface Todo {
  title: string
  completed: boolean
  description: string
}
type TodoPreview = Pick
const todo: TodoPreview = {
  title: 'Clean room',
  completed: false
}

排除

type Exclude = T extends U ? never : T

Exclude的作用与之前介绍的Extract正好相反。 如果T中的类型在U中不存在,则返回,否则丢弃。 现在我们以前面两个类为例,看一下Exclude返回的结果。

interface Worker {
  name: string
  age: number
  email: string
  salary: number
}
interface Student {
  name: string
  age: number
  email: string
  grade: number
}
type ExcludeKeys = Exclude
// 'salary'

取出的是Student中不存在Worker的工资。

忽略

type Omit = Pick<
  T, Exclude
>

Omit的作用与Pick正好相反。 首先,通过Exclude提取类型T中存在而K中不存在的属性,然后根据该属性构造一个新类型。 或者通过上面的Todo案例,TodoPreview类型只需要排除socket的description属性即可,写法比之前的Pick简单。

interface Todo {
  title: string
  completed: boolean
  description: string
}
type TodoPreview = Omit
const todo: TodoPreview = {
  title: 'Clean room',
  completed: false
}