🙅达咩~拒绝搬运文档,从实际出发介绍几个开发中用得上的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