关于TypeScript,你应该知道些什么?(第一篇)
前言
最近在看《Effective TypeScript》,深感自己对TypeScript
的使用和思考还是太过于浅显了,就借此机会写一下自己的一些读书感悟,希望对正在使用TypeScript
的朋友们有点帮助
正文
1. 不要被类型检查欺骗
function asNumber(val: string | number): number {
return val as number
}
// 这时即使类型检查通过了,但经过编译后的js代码仍然可能返回类型为string的值
// 正确做法如下👇
function asNumber(val: string | number): number {
return typeof val === 'string' ? Number(val) : val
}
2. TypeScript
并不会影响编译时的性能,tsc
在将TypeScript
编译为JavaScript
时会将类型清除,因此不存在影响性能的问题
但当遇到需要通过
polyfill
兼容时,tsc
会因需要对代码进行转换而造成一定的性能影响,但不会成为主要原因
3. 当需要对一个对象进行遍历时,可能会出现key
的类型为string
从而造成类型错误的问题
interface Obj {
a: string
b: string
}
function recreate(obj: Obj) {
for (const key in obj) {
const val = obj[key]
// ~~~ 元素隐式具有"any"类型
}
}
可以用下面的方法解决👇
function recreate(obj: Obj) {
let key: keyof Obj
for (key in obj) {
const val = obj[key]
}
}
4. 尽可能少用any
类型
当你设计了下面这段代码的时候,运行时并不会发现任何问题👇
interface ComponentProps {
onSelectitem: (item: any) => void
}
function renderSelector(props: ComponentProps) { ... }
let selectedId: number = 0
function handleSelectItem(item: any) {
selectedId = item.id
}
renderSelector({ onSelectitem: handleSelectItem })
然而当你修改了ComponentProps
时,仍然会通过类型检查
interface ComponentProps {
onSelectItem: (item: number) => void
}
当ts在编译时,上面的代码并不会提示任何错误,但是当handleSelectItem
对象传入的参数为number
类型时,就会造成错误导致代码无法运行
5. 善用tsserver
使用过TypeScript
的朋友们应该很熟悉tsc
,作为编译器,它帮助我们将ts
代码转换为js
代码,但在开发过程中帮助我们检查类型、补全代码的则是tsserver
,在tsserver
的帮助下,我们可以轻松通过编译器来了解类型的构造以及补全代码
// 当我们在使用fetch时,tsserver可以帮助我们跳转到定义fetch类型的源码中,帮助我们了解如何使用fetch
const request = fetch('http://example.com')
// 👆windows下ctrl+鼠标左键,mac下command+鼠标左键
6. 善用集合
如果你希望将一个类型为另一个类型扩充属性,那你可以这样做👇
type Person = { id: string }
type Student = { name: string } & Person // Student类型就同时拥有id和name属性
// 同样的,你也可以使用接口(interface)实现
interface Person { id: string }
interface Student extends Person { name: string } // 也能实现相同的效果
有时候你可能希望函数的参数为对象中的某些属性,那就可以这样做👇
interface Person {
id: string
name: string
age: number
}
type KeyOfPerson = keyof Person // KeyOfPerson类型继承了Person类型的所有属性名
// 👆 类型为'id' | 'name' | 'age'
function inputVal<K extends keyof T, T>(val: T[], key: K) {
...
}
const people: Person[] = [{ id: '1', name: 'Joe', age: 12 }, { id: '2', name: 'Anna', age: 8 }]
inputVal(Person, 'id') // correct!
inputVal(Person, 'name') // correct!
inputVal(Person, 'phone') // fail! 类型'phone'不能赋值给类型为'id' | 'name' | 'age'的参数
同样的,如果你希望某些参数只有在特定的参数值才允许使用时,也可以使用集合实现
type Person = {
name: string
} & ({ gen: 'male', strength: number } | { gen: 'female', charm: number })
const male: Person = {
name: 'Joe',
gen: 'male',
strength: 10
} // correct!
const female: Person = {
name: 'Alice',
gen: 'female',
strength: 10
} // 对象字面量只能指定已知属性,并且“strength”不在类型“{ name: string; } & { gen: "female"; charm: number; }”中
7. 有选择地使用类型声明
和类型断言
首先来了解一下什么是类型声明
和类型断言
👇
interface Person { name: string }
const man: Person = { name: 'Johnson' } // 这是类型声明
const man = { name: 'Johnson' } // 这是类型断言
上面两种方式的结果都是相同的,man
都为Person
类型,但是不同的是:
1⃣️ 类型声明
在对象声明时就确定了对象的值类型必须为Person
2⃣️ 类型断言
则是将赋值给man
对象的值声明为Person
请看下面的例子🌰
const man: Person = { name: 'Johnson', age: 20 } // fail! Person类型中并不包含age
const man = { name: 'Johnson', age: 20 } // correct!
这也就意味着,当你不了解值的具体类型时,不要轻易使用类型断言
但在某些情况下,我们知道值的类型是非常明确的,只是TypeScript
为了保险起见多加了null
类型,那么这个时候就可以使用类型断言
来告诉TypeScript
👇
const dom = document.querySelector('body') as HTMLBodyElement
// 你还可以这样做,同样能达到断言的效果
const dom = document.querySelector('body')!
同样是上面的Person
类型,如果我们需要在一个函数中将返回值设置为Person[]
类型应该怎么做呢?
const people = ['Alice', 'Joe', 'Jcob'].map(name => { name })
// 此时类型为{ name: string }[]
// 类型断言
const people = ['Alice', 'Joe', 'Jcob'].map(name => { name } as Person)
// 类型声明
const people = ['Alice', 'Joe', 'Jcob'].map((name): Person => { name })
上面的两种方式最终都能使people
对象正确声明为Person[]
类型,但是如果当后续修改了Person
类型时,就会发现**类型断言
并不会提示你返回值的类型不正确!**
interface Person { name: string, isAdmin: boolean }
const people = ['Alice', 'Joe', 'Jcob'].map(name => { name } as Person)
// correct!
const people = ['Alice', 'Joe', 'Jcob'].map((name): Person => { name })
// fail! { name: string }类型缺少了Person类型中的age属性
因此,在条件允许的情况下,请尽可能使用类型声明
8. 额外属性检查的局限性
当你对一个已经声明了类型的变量进行赋值时,TypeScript
会确保值的范围只在类型包含的属性中
interface Person { id: string; age: number }
const man: Person = { id: '0', age: 10 }
const woman: Person = { id: '1', name: 'Elsa', age: 8 }
// fail! Person类型中并不包含name属性
但是当你将一个已经声明的对象赋值给变量时,则不会出现这个错误
const info = { id: '1', name: 'Elsa', age: 8 }
const woman: Person = info // correct!
看到这里可能你会说这是不是发现了一个TypeScript
的BUG? 其实并不是,这只是由于info
变量的类型中包含了Person
变量的子集,因此通过了TypeScript
的类型检测
**也就是说,TypeScript
的类型声明校验对于非字面量声明
是不严格的
**
但是这种变化是隐性的,如果不仔细检查可能会忽略,如果你希望Person
类型就是允许有额外的属性的话,那你可以这样做,会更安全些👇
interface Person {
id: string
age: number
[key: string]: string
}
const woman: Person = { id: '1', name: 'Elsa', age: 8 } // correct!
9. 尽可能为整个函数定义类型
在TypeScript
中,不仅可以对变量定义类型,也可以对函数声明类型
type CalcFnc = (a: number, b: number) => number
const plusNumber: CalcFnc = (a, b) => a + b
// 在已经定义了函数的类型后,就不需要在函数声明时重新定义
你可能会说,那我直接在函数声明时对函数的参数和返回值定义类型不行吗?完全没有问题,而且这种方式会更加灵活,但如果你在对某些工具库进行二次封装时,这可能不是一种很好的方法。
例如,在
React
中提供了MouseEventHandler
类型,如果你的修改并不涉及参数的话,那么最好的做法就是将函数定义为MouseEventHandler
类型,而不是将参数定义为MouseEvent
类型,然后再定义返回值类型
再举个例子🌰,现在需要将fetch
函数进行二次封装,需要对请求中可能出现的错误进行单独处理,你可能第一时间会想到这样写👇
async function fetchData (input: RequestInfo, init?: RequestInit) {
const response = await fetch(input, init)
if (!response.ok) {
throw new Error(`Request Error! ${response.status}`)
}
return response
}
但如果并不需要对参数进行拓展的话,下面的写法是更好的选择
const fetchData: typeof fetch = (input, init) => {
const response = await fetch(input, init)
if (!response.ok) {
throw new Error(`Request Error! ${response.status}`)
}
return response
}
你可能会说,这看起来没有区别啊?但事实上并不是这样,当你将函数中的错误处理从throw
修改为return
时,TypeScript
就会捕获到第二个函数的错误,而第一个函数就不会被捕获错误👇
async function fetchData (input: RequestInfo, init?: RequestInit) {
const response = await fetch(input, init)
if (!response.ok) {
return new Error(`Request Error! ${response.status}`)
// 这里并不会被TypeScript检测到任何错误,但是请求发生错误时就不会出现任何错误提示
}
return response
}
const fetchData: typeof fetch = (input, init) => {
const response = await fetch(input, init)
if (!response.ok) {
return new Error(`Request Error! ${response.status}`)
// 不能将类型Promise<Reponse | HTTPError>分配给类型Promise<Response>
// 不能将类型Response | HTTPError类型分配给类型Response
}
return response
}
所以,下次你在声明一个新的函数时,应该先思考一下是否有合适的类型可以用于函数声明,而不是直接给函数的参数和返回值定义类型
10. Type
和Interface
的异同
如果你使用过TypeScript
,那你对这两种类型声明方式肯定不会陌生,在一般情况下,这两种声明方式的作用是一样的
type TNum = { num: number }
interface INum = { num: number }
type TCalNum = (a: number, b: number) => number
interface ICalNum { (a: number, b: number): number }
type TPair<T> = { name: T }
interface IPair<I> { name: I }
上面使用两种方式声明的类型都完全一致,并且interface
和type
也可以互相扩展
interface IProps extends TPair { age: number }
type TProps = IPair & { age: number }
完全没有问题!正如开头所说,一般情况下这两种方式没有任何区别,那他们之间的区别在哪呢?
首先是联合类型,type
可以声明联合类型,而interface
则无能为力
type TCompose = 'A' | 'B'
type TMix = (IPair | TPair) & { add: string }
// 这里为Ipair类型和Tpair类型都添加了一个add属性
但interface
也不是吃素的,type
有他的本领,interface
也有他独特的类型拓展方法
interface IExtends {
isAdmin: boolean
password: string
}
interface IExtends {
username: string
}
const user: IExtends = {
username: 'user',
password: '123',
isAdmin: false
} // correct!
上面我们连续声明了两个IExtends
类型,但他们并不会进行覆盖操作,而是进行合并操作,而type
则相反,连续的声明会覆盖先声明的type
因此,如果你希望编写出来的类型允许被拓展的话,那就使用interface
;如果你不希望类型被拓展,type
是更好的选择。同时,如果仓库的整体风格是使用interface
,那为了保持一致性你也应该尽可能地使用interface
11. 学会使用Readonly
在开始之间,我们先看下面这个例子🌰,并请尝试推断出他的结果是什么
function arraySum(arr: number[]) {
let sum = 0, num
while((num = arr.pop()) !== undefined) {
sum += num
}
return sum
}
function printTriangles(n: number) {
const nums = []
for (let i = 0; i < n; i++) {
nums.push(i)
console.log(arraySum(nums))
}
}
现在公布结果,上面的代码将会输出
0 1 2 3 4
可能有的朋友已经发现了华点,在arraySum
函数中我们使用pop
函数修改了参数arr
的值,由于js
的对象引用导致nums
直接被修改了,因此每次arraySum
被调用时传递的nums
其实都只有一个值
因此对于那些不应该在函数内部修改参数
的函数来说,对参数使用readonly
是一个正确的选择,可以帮助你避免一些不经意间的值修改操作
上面的例子在使用readonly
后就能够很好的解决这个问题
function arraySum(arr: readonly number[]) {
let sum = 0, num
while((num = arr.pop()) !== undefined) {
// ~~~ 类型“readonly number[]”上不存在属性“pop”
sum += num
}
return sum
}
function printTriangles(n: number) {
const nums = []
for (let i = 0; i < n; i++) {
nums.push(i)
console.log(arraySum(nums))
}
}
同样的readonly
也可以作用在赋值上
const sentence: string[][] = []
const word: readonly string[] = ['i', 'love', 'u']
sentence.push(word)
// ~~~~ 类型“readonly string[]”的参数不能赋给类型“string[]”的参数。
如果你希望对象本身是可以修改的,仅是内部的属性不允许修改的话,也可以使用let
声明
let word: readonly string[] = ['i', 'love', 'u']
word = ['i', 'hate', 'u'] // correct!
word[0] = 'me'
//~~~~~ 类型“readonly string[]”中的索引签名仅允许读取。
但需要注意的是,readonly
是浅层的,虽然你不能修改readonly对象
本身,但你可以修改它里面的属性,看下面的例子🌰
const dates: readonly Date[] = [new Date()]
dates.push(new Date())
// ~~~~ 类型“readonly Date[]“不存在“push”
dates[0].setFullYear(2024) // correct!
对于readonly
的兄弟Readonly
也是同样的
interface Outer {
inner: {
x: number
}
}
const o: Readonly<Outer> = { inner: { x: 0 } }
o.inner = { inner: { x: 1 } }
//~~~~~ 不能分配给”inner“,因为它是一个只读属性
o.inner.x = 1 // correct!
对于上面的Readonly<Outer>
来说,创建出来的类型是这样的👇
type T = Readonly<Outer>
// Type T = {
// readonly inner {
// x: number
// }
// }
最后总结一下:
- 如果你的函数不需要修改参数,你应该给参数加上一个
readonly
标志以防意料之外的值操作 readonly
和Readonly
都只作用于浅层对象
结语
目前我还没有读完这本书,所以能写的东西也比较有限,希望能对你有点帮助
如果文中有任何错误或是需要修改的地方,烦请指出,感激不尽
还有我的个人博客,欢迎各位来参观
转载自:https://juejin.cn/post/7254836680132952120