🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的TypeScript技巧
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
}
函数的参数是一个联合类型,我明确知道 flag 是 true 的话,那么 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
}
_
vue3的 setup方法,有两个参数,第一个是 props,第二个参数是一个对象,里面有 emit等方法,有些组件只用到 emit 向外传递事件,props仅用于渲染UI,那么可能会有如下代码
export default defineComponent({
setup(props, { emit }) {
emit('change')
}
})
setup这个方法里没有使用到 props,但为了使用第二个参数的 emit,所以不得不把 props也声明出来,那么你声明了却没有使用,ts会给你报一个 warn
虽然不是 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
我一开始是没搞明白 void 跟 undefined 有什么区别,实际上大部分情况下,二者是可以相互替换的,但如果作为返回值的话,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元素的类型,类似的还有 HTMLLiElement、HTMLCanvasElement等,如果你不知道你想获取的DOM元素到底用哪个类型,可以统一用类型 HTMLElement
const el = ref<HTMLElement>()
但如果这个DOM元素有点不一样,比如是个 input元素,那么 el.value 就该是个 string,如果用 HTMLElement 是会报错的,因为 HTMLElement上没有 value属性,需要用更精确的类型 HTMLElement,如果对于有些元素你实在是不知道其准确类型到底怎么拼写,可以让 ts告诉你
const el = document.createElement('input')
直接在.ts文件里写上这句代码,然后把鼠标移到 el这个变量上,你就会发现 ts已经自动推导出其应该是个什么类型了
注释
程序最不喜欢的两件事情:
- 自己写注释
- 别人不写注释
这恰恰说明了注释是编写可维护代码中不可缺少的一环,按照我的经验,写注释这种事情主要还是习惯问题,如果你习惯性写注释,比如我,经常随手写注释,那么不让我在该写注释的地方写注释,简直比让我在 ts里写 any 还难受
能写注释当然非常好,但如果能把注释写得更有注释意义那就最好不过了,大多数人写注释就是双斜线,vscode 按个组合键 command + / 就能打出来
// 这是注释
function fn(v: string): number {}
如果你使用一些比较知名的库,会发现它们的api注释非常滴银杏化
你在编辑器里打出这个 api的时候,编辑器就自动给你提示出这个 api 的作用、每个参数的类型和作用、返回值的作用,甚至还有demo代码,就算你从来没有看过这个 api的说明,也不需要专门跳转到 api定义的地方,光是从这个提示上就能将这个 api的用法猜得八九不离十了,这就是一个好的注释的意义所在
jsdoc或者 tsdoc 的 api 有很多,这里只说几个我认为有实际意义的
用星号注释
// 这种双斜线注释,是无法在 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()
}
@deprecated
有些函数年久失修,或者有更好的替代函数了,原先的老函数不建议再继续使用了,那么可以使用 @deprecated打个标记
/**
* 字符串日期转为时间戳
* @deprecated
* @param v 字符串日期
* @example
* ```
* fn('2022-10-10') // => 1665360000000
* ```
*/
function fn(v: string): number {
return +new Date(v).getTime()
}
当有不知情的人继续调用这个被废弃的方法时,现代编辑器(例如 vscode)会自动给这个函数划个中划线,ts也会提示此方法已弃用
小结
绝大部分情况下,业务代码根本不需要多么高深的技巧,脚踏实地的关注基本的细节即可维护好一份质量不错的代码项目,但最常见的场景是,很多人是一边硬编码魔术字符串一边大谈设计模式
转载自:https://juejin.cn/post/7171316430938308615