likes
comments
collection
share

🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧

作者站长头像
站长
· 阅读数 59

typescript 很大程度上提升了项目的可维护性和程序员的开发体验,关于 ts的各种骚操作也层出不穷,例如 TypeScript 类型体操天花板,用类型运算写一个 Lisp 解释器用 TypeScript 类型运算实现一个中国象棋程序ts的知识量还是很大的,本文仅立足于实际开发场景,根据本人日常开发经验,介绍一些广大一线搬砖工们能够用得上、看得懂的简单技巧,可看做是typescript在实际项目中的运用续篇

is

刚学 ts的时候,我是不理解 is有啥用的,直到我遇到了一个问题

function fn(v: number) {
  return [1, 2, v > 10 ? 10 : undefined, 3].filter(v => typeof v === 'number')
}

对于上述函数 fn,我可以很确定其返回值类型是 number[],但 ts自动推导却是 (number | undefined)[],这显然不对,函数已经把 undefined 过滤掉了,固然可以通过 as number[] 这种手段来强制返回一个 number[],但用强总感觉不是那个味,然后我搜了下,发现 is可以解决这个问题

function fn(v: number) {
  return [1, 2, v > 10 ? v : undefined, 3].filter((v): v is number => typeof v === 'number')
}

还有一种实际开发中经常遇到的场景也能用到 is

function fn(flag: boolean, v: TA | TB) {
  return flag ? v.a : v.b
}

函数的参数是一个联合类型,我明确知道 flagtrue 的话,那么 v必然是 TA类型,否则就是 TB类型,但是 ts不知道啊,可以用 as TA的方式让 ts乖乖接受,但最好是用 is

const isA = (f: boolean, v: TA | TB): v is TA => f

function fn(flag: boolean, v: TA | TB) {
  return isA(flag, v) ? v.a : v.b
}

_

vue3setup方法,有两个参数,第一个是 props,第二个参数是一个对象,里面有 emit等方法,有些组件只用到 emit 向外传递事件,props仅用于渲染UI,那么可能会有如下代码

export default defineComponent({
  setup(props, { emit }) {
    emit('change')
  }
})

setup这个方法里没有使用到 props,但为了使用第二个参数的 emit,所以不得不把 props也声明出来,那么你声明了却没有使用,ts会给你报一个 warn

🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧 虽然不是 error可以不用管,但画个黄线看着也实在不爽,把 no-unused-vars的规则禁用掉当然可以,但未免有些暴力,其实如果你是在 tsx文件里这么写的话, ts校验器会给解决方案的: Allowed unused args must match /^h$|^_/u

意思是如果声明变量而不用还不想被 warn的话,这个变量名必须是 h 或者以 _开头,也就是写成下面两种变量名就可以了

setup(h, { emit }) {}
setup(_, { emit }) {}

下划线的这种形式,在 Go里面也有,Go函数可以返回多个值,接收的时候必须全都接收,一旦接收了就必须得使用,否则编译不通过,如果有值你真的用不到,那么就可以使用下划线 _作为变量名来规避这种检查

_, value := fn()

void

我一开始是没搞明白 voidundefined 有什么区别,实际上大部分情况下,二者是可以相互替换的,但如果作为返回值的话,void可以用不同的类型替换,以允许高级回调模式,这在一些第三方库的方法中会比较常见,对于我们业务开发人员来说,这个特性可以用在一些工具方法中

export function utilFn(callback: () => void) {
  callback()
}

工具方法 utilFn接收一个函数作为参数,如果把这个函数参数的返回值类型设为 void,意味着 callback 这个函数返回任何值或者不返回任何值都是合法的

utilFn(() => {
  if (v > 10) return
  console.log(v)
})
utilFn(() => 'a')
utilFn(() => {
  console.log(1)
})

如果把 callback: () => void中的 void换成 undefined,上面三种调用都会报错,因为现在要求 callback必须返回一个 undefined类型的值才行;你也可以把 void换成 any,但这在语义上就不太能说得通了,any也是一种类型,潜台词是 callback必须返回一个什么值哪怕是 undefined也行,但实际上你的本意是不关心callback的返回的

tuple 元组

tuple allow each element in the array to be a known type of value,在我看来,这句话的意思是在数组类型的基础上,进一步精确了类型

let dateRange: [string, string] = ['2022-10-10', '2022-10-11']

例如有一个用于表示日期范围的变量,可以确定的是这个变量肯定是一个包含且仅包含两个字符串的数组,那么把这个变量定义为 string[]当然是可以的,但如果更精确点,[string, string] 会更好一点

这会带来更强的类型安全和编码可读性,dateRange[2]dateRange = []等操作在编译层面就已经非法了且是符合预期的,任何一个开发者看到这个变量也都会对这个变量有更准确地认知,不会怀疑这个变量会不会是个空数组?会不会只有一个子项?避免了无意义的判断和可能失败的操作

HTMLElement

现代前端开发基本上都是基于 react/vue这种数据驱动框架,一般不需要手动操作 DOM API,但有些时候还是需要的,例如在 vue 3.x中可以通过 ref来获取 DOM元素的实例

const el = ref()

此时的 el是默认的 any 类型,这当然没啥大问题,但作为 ts践行者,看到 any 就浑身不舒服,ts 已经给 DOM 元素实例内置了类型,不用上岂不是辜负了 ts的一片好心?

const el = ref<HTMLDivElement>()

onMounted(() => {
  // 可以自动推断出 height 是 number 类型了
  const height = el.value!.offsetHeight
})

HTMLDivElement 指的是一个 div元素的类型,类似的还有 HTMLLiElementHTMLCanvasElement等,如果你不知道你想获取的DOM元素到底用哪个类型,可以统一用类型 HTMLElement

const el = ref<HTMLElement>()

但如果这个DOM元素有点不一样,比如是个 input元素,那么 el.value 就该是个 string,如果用 HTMLElement 是会报错的,因为 HTMLElement上没有 value属性,需要用更精确的类型 HTMLElement,如果对于有些元素你实在是不知道其准确类型到底怎么拼写,可以让 ts告诉你

const el = document.createElement('input')

直接在.ts文件里写上这句代码,然后把鼠标移到 el这个变量上,你就会发现 ts已经自动推导出其应该是个什么类型了

🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧

注释

程序最不喜欢的两件事情:

  1. 自己写注释
  2. 别人不写注释

这恰恰说明了注释是编写可维护代码中不可缺少的一环,按照我的经验,写注释这种事情主要还是习惯问题,如果你习惯性写注释,比如我,经常随手写注释,那么不让我在该写注释的地方写注释,简直比让我在 ts里写 any 还难受

能写注释当然非常好,但如果能把注释写得更有注释意义那就最好不过了,大多数人写注释就是双斜线,vscode 按个组合键 command + / 就能打出来

// 这是注释
function fn(v: string): number {}

如果你使用一些比较知名的库,会发现它们的api注释非常滴银杏化

🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧 你在编辑器里打出这个 api的时候,编辑器就自动给你提示出这个 api 的作用、每个参数的类型和作用、返回值的作用,甚至还有demo代码,就算你从来没有看过这个 api的说明,也不需要专门跳转到 api定义的地方,光是从这个提示上就能将这个 api的用法猜得八九不离十了,这就是一个好的注释的意义所在

jsdoc或者 tsdocapi 有很多,这里只说几个我认为有实际意义的

用星号注释

// 这种双斜线注释,是无法在 fn 的调用处提示给调用者的
function fn(v: string): number {}
/**
 * 这种星号注释,可以在 fn 的调用处提示给调用者
*/
function fn(v: string): number {}

@params

解释清楚每个参数的意义

/**
 * 字符串日期转为时间戳
 * @param date 字符串日期
 */
function fn(date: string): number {
  return +new Date(v).getTime()
}

@example

有些方法写得比较复杂,别人不太容易搞懂是干什么的,入参、出参也一头雾水,那么这时候就有必要提供一个 demo

/**
 * 字符串日期转为时间戳
 * @param v 字符串日期
 * @example
 * ```
 * fn('2022-10-10') // => 1665360000000
 * ```
 */
function fn(v: string): number {
  return +new Date(v).getTime()
}
🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧

@deprecated

有些函数年久失修,或者有更好的替代函数了,原先的老函数不建议再继续使用了,那么可以使用 @deprecated打个标记

/**
 * 字符串日期转为时间戳
 * @deprecated
 * @param v 字符串日期
 * @example
 * ```
 * fn('2022-10-10') // => 1665360000000
 * ```
 */
function fn(v: string): number {
  return +new Date(v).getTime()
}

当有不知情的人继续调用这个被废弃的方法时,现代编辑器(例如 vscode)会自动给这个函数划个中划线,ts也会提示此方法已弃用

🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧

小结

绝大部分情况下,业务代码根本不需要多么高深的技巧,脚踏实地的关注基本的细节即可维护好一份质量不错的代码项目,但最常见的场景是,很多人是一边硬编码魔术字符串一边大谈设计模式

转载自:https://juejin.cn/post/7171316430938308615
评论
请登录