学以致用的 TypeScript 技巧

259 阅读2分钟

遍历 Object 对象

interface IPerson{
    name: string,
    age: number,
    hobbies: string[]
}
const p: IPerson = {
    name: 'zs',
    age: 12,
    hobbies: ['reading', 'shopping']
}

对于实例 p 循环遍历的三种方式:
方式一:Object.keys 方式

type pKeys = keyof IPerson
const keys = Object.keys(p) as pKeys[]
keys.forEach((key)=>{
    console.log(key, p[key])
})

方式二:for in

type pKeys = keyof IPerson
let key: pKeys
for(key in p){
  console.log(key, p[key])
}

方式三:Object.entries

for(const[k, v] of Object.entries(p)){
  console.log(k, v)
}

注意:这种方式获取的对象值 vany 类型,使用时需要注意类型判断。

枚举类型取值

后端一般会给我们如下的枚举类型:

enum EnumScheduleType {
  WORK_SCHEDULE = '工作班次',
  REST_SCHEDULE = '休息班次',
}

在我们得到的数据可能直接是 scheduleType 为 WORK_SCHEDULE 或者 REST_SCHEDULE,这就需要将这些枚举转为用户能看懂的文字信息。如果我们直接通过 EnumScheduleType[scheduleType] 的方式获取对应的文字信息的话,编译器会告诉我们如下的错误:

image.png

意思就是不能随便使用 string 类型去匹配 EnumScheduleType 的值,正确如下:

// 定义 EnumScheduleType key 的范围
type ScheduleTypeStrings = keyof typeof EnumScheduleType
// 等价于
// type ScheduleTypeStrings = 'WORK_SCHEDULE' | 'REST_SCHEDULE';

// 后端获取数据,准确定义好其类型 
const scheduleType: ScheduleTypeStrings = getFromServer()

// 获取值
const scheduleName = EnumScheduleType[scheduleType]

Vue 中 ref 组件类型 (InstanceType)

组件 HelloWorld.vue

// HelloWorld.vue
<template>
<p>{{msg}}</p>
</template>
<script lang="ts">
export default {
  props: {
    msg: String
  },
  setup(props: any) {
    const print = ()=>{
      console.log(props.msg)
    }
    return{
      print
    }
  }
}
</script>

ref 使用该组件:使用 InstanceType 定义组件 HelloWorld.vue 的类型

// App.vue
<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld
    ref="helloRef"
    msg="Welcome to Your Vue.js + TypeScript App"
  />
</template>

<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { ref, onMounted, } from 'vue'

const helloRef = ref<InstanceType<typeof HelloWorld>>()

onMounted(()=>{
  // 这里可以获取组件 HelloWorld 的 print 方法
  helloRef.value?.print()   
  // Welcome to Your Vue.js + TypeScript App
})
</script>

对于 Vue 3.2 之后的组件,需要将暴露的属性通过 defineExpose 暴露出去。

使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏:

// HelloWorld.vue
<script lang="ts" setup>
interface Props{
  msg: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(),{
  msg: 'hello',
  labels: ()=> ['one']
})

const print = ()=>{
  console.log(props.msg)
}

defineExpose({
  print
})
</script>

! 非空断言

在 TS 中 !用于告诉编译器变量不存在 undefinednull 的情况。

function myFunc(maybeNumber: number | undefined | null) {
  // Type 'number | null | undefined' is not assignable to type 'number'.
  // Type 'undefined' is not assignable to type 'number'. 
  const onlyNumber: number = maybeNumber;   // Error
  const ignoreUndefinedAndNull: number = maybeString!;  //no problem
}

常用于开发者明确知道某个变量不是 undefinednull 的场景,有可能某个变量在最开始定义的时候为 undefined,中间已经完成了赋值,但是编译器还是会报错,这个时候可以使用 ! 明确其不存在 undefinednull 的情况。

interface IPersonInfo{
  name: string,
  age: number,
}
const person = ref<IPersonInfo>()
// 先进行赋值操作
const initPerson = (record: IPersonInfo)=>{
  person.value = record
}
// ! 非空断言
const test = ()=>{
  person.value!.name = 'Lily'
}

Provide/Inject + TypeScript 使用

参见博客:juejin.cn/post/700033…

索引签名 VS Record 类型

索引签名和 Record 类型都可以用来定义一个对象(Object)的 keyvalue 取值类型,使用上有些不同而已。

索引签名

索引签名的详细用法可以参考:www.jianshu.com/p/b00ad373f… 在 TypeScript 中语法如下:

type Human = {[key: string]: string}
// 等价  type Human = {[name: string]: string}
const man: Human = {
  name: 'zhangsan',
  job: 'worker',
}

其中 key 是用来增加可读性,并没有实际意义,也可以用其他字段表示。

可以限制 key 的取值范围:

type PositionIndex = 'x' | 'y'
type Position = {[k in PositionIndex]: number}
const point1: Position = {
  x: 10,
  y: 10
}

这里 Positionkey 必须包括 PositionIndex 里所有取值,既要有 x 属性,又要有 y 属性: image.png

可以使用 ?: 可选属性让 key 的取值为可选:

type PositionIndex = 'x' | 'y'
type Position = {[k in PositionIndex]?: number}
const point1: Position = {
  x: 10,
}

与索引签名一起使用时,必须保持与索引签名一致的类型:

image.png

Record 类型

type People = Record<string, string>
const woman: People = {
  name: 'lisi'
}

限制对象 key 的取值:

type PositionIndex = 'x' | 'y'
type Position = Record<PositionIndex, number>
const point1: Position = {
  x: 10,
  y: 1
}

同样这里的 point1 必须要有 xy 两个属性:

image.png

与索引签名不同的是Record 类型并不能实现可选属性。

相同点是索引签名 和 Record 类型的 key 取值类型必须是 string | number | symbol

必须包括所有属性的理解

type PositionIndex = 'x' | 'y'
type Position = {[k in PositionIndex]: number}
const point1: Position = {
  x: 10,
  y: 10
}
// 或者
type PositionIndex = 'x' | 'y'
type Position = Record<PositionIndex, number>
const point1: Position = {
  x: 10,
  y: 1
}

上面两种写法都需要 point1 必须要有 xy 两个属性,怎么理解呢?可以借用 keyof,

image.png

当我们使用 keyof 来获取 Position 的 key 时,会发现跟

type PositionIndex = 'x' | 'y'
// 等价
type AllKey = keyof Position

所以 point1 必须要有 xy 两个属性

try...catch 错误信息提示

try...catch 语句中,有可能 catcherror 变量不是 Error 类型,要加一层判断:

try {
 // ...
} catch (err) {
  let message = '操作失败'
  if (err instanceof Error) {
    message = err.message
  }
  alert(message)
}

try...catch 返回类型处理

async...await 中的 try...catch 异常处理,需要返回 Promise<T> 的类型。这个时候的 catch 有两种处理方式。

一种是在 catch 里面 return 对应的类型,另一种是 throw(error)

let myPromise = Promise.resolve('Resolved Data');

async function myFunction(): Promise<string> {
    try {
        let resolvedValue = await myPromise;
        return resolvedValue;
    } catch (error) {
        console.log(error);
        //return Promise.resolve('test')
        throw(error)
    }
}

函数

type 定义函数

type SearchFn = (keyword: string) => void
const searchName: SearchFn = (name: string)=>{
    console.log(name)
}

interface 定义函数

interface ISearchFn{
    (key: string): void    
    // use `:` between the parameter list and the return type rather than `=>`.
}
const search: ISearchFn = (name: string)=>{
    console.log(name)
}

注意 使用 : 在函数的参数和返回类型之间,而不是 =>

属性是函数类型

type Pagination = {
    total: number
    current: number
    pageSize: number
    showTotal: (total: number) => void
}

interface IPagination {
    total: number
    current: number
    pageSize: number
    showTotal: (total: number) => void
}

const paginationParams: Pagination = {
    total: 0,
    current: 1,
    pageSize: 10,
    showTotal: total => `共${total}条`,
}

获取字段类型

获取 interface 某个字段类型声明,hobbyT 就是 string | number

interface IPerson{
  name: string
  hobby: string | number
}

type hobbyT = IPerson['hobby']

获取 type 某个字段类型声明,hobbyData 就是 boolean | number

type TableData = {
  id: number,
  name: string,
  hobby: boolean | number
}

type hobbyData = TableData['hobby']

?? 空值合并运算符

?? 是一个逻辑运算符,当左侧的操作数是 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

与逻辑或运算符(||)不同,逻辑或运算符会在左侧操作数为 falsy 时,返回右侧操作数。

所以, ?? 相比较 || 只限制 null 和 undefined,更实用。

console.log( 0 ?? 42)  // 0
console.log( 0 || 42)  // 42

# TypeScript 强大的类型别名
# 细数 TS 中那些奇怪的符号