typescript在实际项目中的运用
最近在公司技术群里发现一个 typescript体操题库,之前虽然也用 ts
但只是当成一个纯粹的类型注释来用,也听说过 ts
的类型可编程性,但一直不解其意,直到我看到这个库后大感震撼,零零碎碎刷了大半题目之后,这才对于 ts
有了完全不一样的认知
当然了,那个题库里题目所用到的体操技巧,对于写业务代码的人来说很难用上几个,关于 ts
的基本语法看官方文档即可,我就不浪费资源再去 repeat
了,本文分享几个我认为在实际写业务代码的时候可能用上的 ts
写法
Const Assertions
TypeScript 3.4
提供的一个功能 const
断言,特性描述:
- 文字表达式中的文字类型都不会扩展(即不能重新赋值)
- 对象属性只读
- 数组变成只读的元组
举个例子
const arr = [1, 2, 3] as const;
arr.push(4);
这个时候,编辑器会报个错
因为此时 arr
不再是纯粹的数组了,而是 元组
,其类型也由 number[]
变成了 readonly [1, 2, 3]
再来个例子:
const obj1 = {
a: 1,
b: 2
}
obj1.a // number
const obj2 = {
a: 1,
b: 2
}
obj2.a // 1
不加 const
修饰,则 obj1.a
的类型就是 number
;加了之后,因为 obj2.a
是 readonly
的,所以编辑器可以很确定地告诉你 obj2.a
的值和类型都是 1
明确对象属性的值及其类型,会带来一个比较方便的类型定义用法
const obj = {
a: 1
}
type TSome = {
[obj.a]: number // 不行
}
上述这个写法,编辑器会直接给你报个错
obj.a
的类型是 number
,是一个并不具体的类型,无法作为 computed property name
所以上面的写法不行,但是你多给 obj
后面加个 const
修饰之后,obj.a
的类型就是 1
,这是个具体的类型,那就行
const obj = {
a: 1
} as const
type TSome = {
[obj.a]: number // 行
}
在实际项目中,此特性主要用于定义不可变的对象变量
另外,如果对 enum
使用 const
修饰也会产生特别的效果
enum X {
A,
B
}
const item = X.A
// 将被编译成
var X;
(function (X) {
X[X["A"] = 0] = "A";
X[X["B"] = 1] = "B";
})(X || (X = {}));
var item = X.A;
加上 const
修饰之后
const enum X {
A,
B
}
const item = X.A
// 将被编译成
var item = 0 /* A */;
enum
经过 const
修饰之后,enum
对应的变量会直接从编译结果中被删掉,从 enum
中取的值都会被编译成对应的常量,理论上来说代码体积会小一点,性能会更高一点,毕竟少了一个变量也少了从变量取值的过程
交叉类型 &
实际业务中,主要用于合并第三方库提供的类型
例如,如果项目是 react
技术栈并且使用到了 react-router
的话,页面组件的 props
上基本上都会有 router
相关的方法和属性,如果是一个个手动在 props
上添加这些router
属性未免太过麻烦及繁琐,好在 react-router
已经给我们提供了相关类型
import { RouteComponentProps } from 'react-router-dom';
function About(props: RouteComponentProps & {
title: string;
}) {
props.history.push('/home');
}
keyof、typeof
keyof
: 将一个类型映射为它所有成员名称的联合类型
typeof
: 用于获取变量的类型
我一般主要用这两个来关联数组与对象,例如数组的项是对象的key
,或对象的 key
是数组的项
const tabList = ['home', 'about'] as const
// const tabMap = tabList.reduce((t, v) => (t[v] = '/mobile/' + v, t), {} as Record<string, string>)
// const tabMap = tabList.reduce((t, v) => (t[v] = '/mobile/' + v, t), {} as any)
我希望希望 tabMap
的类型是{ home: string; about: string }
,这样我就能在编辑器里 .
出来这两个属性而不是我自己去看有哪些属性,但上述代码中两种写法分别是 Record<string, string>
、any
,当然可以通过显式声明的方式来断言 tabMap
的类型:
const tabMap = tabList.reduce((t, v) => (t[v] = '/mobile/' + v, t), {} as { home: string, about: string })
但后续如果我修改了 tabList
的值,就得同时修改 tabMap
的显式类型了,明显是存在耦合的,那么借助 typeof
换个写法:
const tabMap = tabList.reduce((t, v) => (t[v] = '/mobile/' + v, t), {} as Record<(typeof tabList)[number], string>)
现在,无论 tabList
的值怎么改,tabMap
的类型都能自动保持一致了
再来一个例子
const vehicleTypeMap = {
car: 1,
bus: 2,
plane: 3,
ship: 4
}
function fn(vehicleType) {}
对于 fn
方法的 vehicleType
参数,只接收值为 vehicleTypeMap
中的属性值,即 1
、2
、3
、4
你可能说把 vehicleTypeMap
定义成一个 enum
不就行了吗?在本例子中当然是可以的,但实际场景中,vehicleTypeMap
的 key
可能都是有其他作用的,比如 key
都是具有实际意义的,可能需要遍历这些 key
,所以不能直接定义成一个 enum
,但又希望取出这些 key
的值,那么可以借助 keyof
、typeof
const vehicleTypeMap = {
car: 1,
bus: 2,
plane: 3,
ship: 4
} as const
type TVehicleType = (typeof vehicleTypeMap)[keyof typeof vehicleTypeMap] // type => 1 | 2 | 3 | 4
function fn(vehicleType: TVehicleType) {}
模板字符串类型
这是 Type 4.1 的特性,可用于收窄字符串相关类型
例如,你写了一个 fetchAuth
方法,其中包含了一些特殊逻辑,只给鉴权相关接口使用,而鉴权相关接口的 path
上都会带有 /auth
的前缀,那么就可以通过控制传入的接口path
来限制只允许鉴权相关的接口路径被传入:
function fetchAuth(path: `/auth/${string}`) {}
fetchAuth('/auth/login') // ok
fetchAuth('/api/home') // Argument of type '"/api/home"' is not assignable to parameter of type '`/auth/${string}`'.
对于如下代码:
const str1 = 'a'
const str2 = 'b'
const str3 = `${str1}${str2}`
你会发现 str1
的类型是 a
,str2
的类型是 b
,但是 str3
的类型却是 string
,但是我们都知道 str3
的值一定是 ab
,所以它的类型一定是 ab
,我想让ts
自动推导 str3
的类型是 ab
而不是string
该怎么做呢?
你可以做个体操,但实际上不用那么麻烦,借助上面说过的 as const
即可
const str1 = 'a'
const str2 = 'b'
const str3 = `${str1}${str2}` as const
现在 str3
的类型就是 ab
了
Pick
从一个复合类型中,取出几个想要的类型的组合
interface IApiData {
code: number
data: {
name: string
age: number
gender: number
}
msg: string
}
type TData = Pick<IApiData, "data">
如上,从 IApiData
取出 data
的类型
不过,我想说的不是这个
对于上面的例子,实际工作场景中,绝大部分情况下想得到的类型是:
type Data = {
name: string
age: number
gender: number
}
而不是 Pick<IApiData, "data">
的结果:
type Data = {
data: {
name: string;
age: number;
gender: number;
};
}
那么怎么办呢?其实很简单,根本不用 Pick
type TData = IApiData["data"]
以我的经验看,这比 Pick
使用的场景更加广泛和常见,例如传递 history
:
import { RouteComponentProps } from 'react-router-dom';
function Child(props: { text: string; history: RouteComponentProps["history"] }) {
// ...
}
function About(props: RouteComponentProps) {
return <Child text="child" history={props.history}>
}
泛型约束 extends
主要是利用其可收窄类型范围的特性,让类型更加精确
比如,对于一个函数 fn
,其接受一个参数,这个参数至少包含一个 name
属性,如果你这么写可能是无法覆盖所有场景的:
function fn(param: { name: string }) {}
上述写法意味着 fn
的参数param
只能有一个 name
属性,多了少了都不行
你可能想到可以这么写:
function fn(param: { name: string, [propName: string]: any }) {}
fn({ name: 'foo', age: 18 }) // ok
但如果借助泛型约束的话会更直观:
function fn<T extends { name: string }>(param: T) {}
fn({ name: 'foo', age: 18 }) // ok
再来个例子,函数接收一个string
或 number
类型的参数,返回值的类型与参数类型保持一致,使用联合类型或者单纯的泛型都是不够的:
function fn1(value: string | number) {
return value
}
fn1(1) // type => string | number
function fn2<T extends string | number>(value: T) {
if (typeof value === 'number') {
return value * 10
}
return value + ' world'
}
fn2(1) // type => string | number
这个时候就需要泛型约束了:
function fn3<T extends string | number>(value: T) {
let result: unknown
if (typeof value === 'number') {
result = value * 10
} else {
result = value + ' world'
}
return result as T extends string ? string : number
}
fn3(1) // type => number
fn3('') // type => string
类似这种使用三元表达式计算类型的场景(即形如Y extends X ? A : B
),也叫做条件类型,一般都是 三元表达式 + extends
的格式
条件类型在结合联合类型使用时(只针对 extends
左边的联合类型),条件类型会被自动分发成联合类型,这种条件类型也称为分布式条件类型
(string | number) extends T ? A : B
// 相当于
(string extends T ? A : B) | (number extends T ? A : B)
如果不了解这个特性的话,对于上面的 fn3
,可能会觉得是有问题的:
function fn4(value: string | number) {
fn3(value)
}
如果条件类型没有分发的特性,那么对于上述例子中 fn3(value)
来说,由于 (string | number) extends string ? string : number
的结果是 number
,所以 fn3(value)
的类型就是 number
,显然这个类型是不对的,但实际上由于分发特性的存在,fn3(value)
的类型是 string | number
,这是符合预期的
分布式条件类型也是有条件的,待检查的类型(即extends左边的类型)必须是裸类型(naked type parameter)。即没有被诸如数组,元组或者函数包裹
// 以下并不会发生类型分发
Array<string | number> extends T ? A : B
类型的命名
通常情况下,我定义一个 interface
类型,类型的名称会以 I
开头,例如 IApiData
;定义的 type
类型,类型的名称会以 T
开头,例如 TData
;同理,enum
类型就以 E
开头。某种编程语言好像是有这种约定俗成的规定,我忘记是哪种语言了(C#
?)
在我看来,这种写法有两种好处
- 直观,一眼看上去我就知道这是个类型而不是变量
import { selectTypeList, verifyPathMap, CONTROL_TYPES, selectTypeItem, controlType } from 'conf'
以上这行导入,不点进去看谁能知道哪个是变量哪个是类型? 如果换成:
import { selectTypeList, verifyPathMap, CONTROL_TYPES, TSelectTypeItem, IControlType } from '../conf'
那就很清楚了
- 变量命名困难症福音
我有个变量叫 selectTypeItem
,我想给它定义一个类型,总得给类型起个名字吧,这个名字不能是 selectTypeItem
,但最好又要让人一眼看上去就知道跟 selectTypeItem
有关联,叫啥好呢,就在 selectTypeItem
前面加个 I
/T
岂不美哉?
小结
从我的经验来看,typescript
毫无疑问可以提升项目代码的可维护性,但必须确保你能把它用好,如果到处都是 as any
,typescript
可能就变成了一种负担
转载自:https://juejin.cn/post/6995786330533806117