一、类型
unknown
unknown
指的是**不可预先定义的类型,**我们声明unknown,即表明:这个业务虽然是我开发的,但我并不晓得这个变量是啥类型,你们之后来维护的人,不要甩锅给我。 unknown可以让开发人员直观的认识到,某个变量的类型是未知的。 unknown 的一个使用场景是,避免使用 any 作为函数的参数类型而导致的静态类型检查 bug:
function test(input: unknown): number {
if (Array.isArray(input)) {
return input.length; // Pass: 这个代码块中,类型守卫已经将input识别为array类型
}
return input.length; // Error: 这里的input还是unknown类型,静态检查报错。如果入参是any,则会放弃检查直接成功,带来报错风险
}
void
在dota2中,这是一个英雄的名字,叫“虚空”,但是在TS中,void表示,我什么都不在意。 void最常见的使用场景是在返回值中,表示:**随便你返回什么值都可以,我无所谓。**如:
type TestViod = () => void
const a:TestVoid = () => {
return '111' // 并不会报错
}
当我们随便写一个函数, 此时缺省返回类型就是void
function a() {}
never
一段代码,没法正常返回,那就是never类型。 如: 报错、死循环
function test(): never { throw new Error('error message') } // throw error 返回值是never
function test(): never { while(true){} } // 这个死循环的也会无法正常退出
还有就是永远没有相交的类型:
type human = 'boy' & 'girl' // 这两个单独的字符串类型并不可能相交,故human为never类型
二、运算符
非空断言运算符 !
这个 !
可以放在函数或者变量后面,表示,这个函数、变量是不为 null | undefined 的 我遇到过这样的报错:
这时候,如果我们使用
!
运算符,就表示,这个对象是真实存在滴,报错就消失了。 如:
function test(callback?: () => void) {
callback!(); // 参数是可选的,如果不加!运算符,ts会报错
}
编译后的 ES5 代码,居然没有做任何防空判断。(手动伏笔)
function onClick(callback) {
callback();
}
!
最多的使用场景,就是我们明确知道某个东西是不为空的,可以少写if判断。如React的Ref
function Demo() {
const divRef = useRef<HTMLDivElement>();
useEffect(() => {
divRef.current!.scrollIntoView(); // 当组件Mount后才会触发useEffect,故current一定是有值的
}, []);
return <div ref={divRef}>Demo</div>
}
可选链运算符 ?.
**并不是 **?**
,而是 **?.**
**
相比非空运算符 !
, ?.
是更有效的运行时非空判断,因为会做代码转换。
obj?.prop obj?.[index] func?.(args)
?.用来判断左侧的表达式是否是 null | undefined,如果是则会停止表达式运行,可以减少我们大量的&&运算。 比如我们写出a?.b时,编译器会自动生成如下代码
a === null || a === void 0 ? void 0 : a.b;
空值合并运算符 ??
这个运算符,可以有效解决上周 媛媛同学所说的,判断后端返回的值为空的问题 ??
与 ||
功能比较相似,区别在于 ??在左侧表达式结果为 null 或者 undefined 时,才会返回右侧表达式 。 比如我们写了 let b = a ?? 10
,生成的代码如下
const b = a !== null && a !== void 0 ? a : 10
用 ??
判断,就不用考虑 0
了, 0 也会为 true
数字分隔符 _
对于很长的数字,如果进行分割,可能看起来更有格式,比如手机号
const phone: number = 133_1234_1234
这个符号只是并不影响数字本身,无色无味,优化于无形之中,是可以放心食用的
三、操作符
keyof 获取type或interface的对象键
interface a {
m: string;
x: number;
};
type t = keyof a; // t 就是 'm' | 'x'
keyof比较常用的一个场景就是,当我们要获取对象的值,而我们不确定key有哪些时,可以这样搞
function getValue (p: Person, k: keyof Person) {
return p[k]; // 如果k不如此定义,则无法以p[k]的代码格式通过编译
}
总结keyof的用法:
类型 = keyof 类型
typeof 获取实例的类型
什么叫实例? 在java这种后端语言中构造函数new出来的对象,就叫做实例(是老师教的)。在js中,一个普通对象也叫做实例 typeof让我们可以很方便的获取到对象的类型,比如:
const me: Person = { name: 'lsp', age: 24 };
type P = typeof me; // { name: string, age: number | undefined }
const you: typeof me = { name: 'sg', age: 18 } // 可以通过编译
ok,我们现在获取到了一个类型,是不是可以用keyof,再来获取到对象的key呢?
type PersonKey = keyof typeof me; // 'name' | 'age'
总结typeof的用法:
类型 = typeof 实例对象
遍历属性 in
这个方法,也是比较好用的,遍历一个对象的key:
type Test<T> = {
[key in keyof T]: number
}
总结in的用法
[ 自定义变量名 in 枚举类型 ]: 类型
四、泛型
新后台中,我们可以看到非常多泛型的使用,比如:
const [t, setT] = useState<string>() // <string>
const ref = useRef<HTMLDivElement>() // <HTMLDivElement>
const Component:React.FC<someType> = () => {} // <someType>
这些都是我们使用别人定义好的泛型。 为什么会有泛型? 所谓,一生二,二生三,三生万物。如果我们一开始就把某个东西定死了,那么它就没有扩展性了。泛型,即是为扩展性而生的。
基本使用
如何定义泛型?
比较常见的有: 普通类型定义、函数定义。 不常见的有类定义。
// 普通类型定义
type Dog<T> = { name: string, type: T }
// 普通类型使用
const dog: Dog<number> = { name: 'ww', type: 20 }
// 函数定义
function swipe<T, U>(value: [T, U]): [U, T] {
return [value[1], value[0]];
}
// 函数使用
swipe<Cat<number>, Dog<number>>([cat, dog])
泛型推导
我们可以简化对泛型类型定义的书写,因为TS会自动根据变量定义时的类型推导出变量类型。如:
const [test, setTest] = useState<string>() // test 默认为string
// 如果我们强行设置为其他类型,会报错
setTest(1)
虽然但是,不建议这样做,对于维护代码的人来说,看到这种没有定义的,还得自己去想一下类型,有点脑壳痛
泛型约束
大部分时候,我们用泛型,就是为了不限制类型,但是 有时候,有时候,我会限制泛型的类型 一个比较Σ(☉▽☉“a蠢的例子, 这是最简单的使用方式
function test<T extends number>(value: T): number {
return value++
}
加个难度, 遍历对象
function test<T, U extends keyof T>() {}
泛型条件
就是个三元运算符,业务中比较少用到
T extends U ? X : Y
如果是 U 子类型,则将 T 定义为 X 类型,否则定义为 Y 类型。
泛型工具
Partial
此工具的作用就是将泛型中全部属性变为可选的。
type Partial<T> = {
[key in keyof T]?: T[P]
}
使用场景: 在dva中:
Pick<T, K>
此工具的作用是将 T 类型中的 K 键列表提取出来,生成新的子键值对类型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
比如,我想把一个类型中的某几个key,取出来(就不用我手动去复制粘贴了)。
type Person = {
name: string,
age: number,
eat: () => number,
work: () => void
}
type Test = Pick<Person, 'name' | 'age'>
const a: Test = { name: 'xxx', age: 14 }
Record<K, T>
此工具的作用是将 K 中所有属性值转化为 T 类型,常用它来申明一个普通 object 对象。
type Record<K extends keyof any,T> = {
[key in K]: T
}
比如,我想声明一个value是任意类型的对象
type anyObject = Record<string, any>
Exclude<T, U>
此工具是在 T 类型中,去除 T 类型和 U 类型的交集,返回剩余的部分。
type Exclude<T, U> = T extends U ? never : T
这里的 extends 返回的 T 是原来的 T 中和 U 无交集的属性,而任何属性联合 never 都是自身,具体可在上文查阅。 比如:
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
Omit<T, K>
此工具可认为是适用于键值对对象的 Exclude,它会去除类型 T 中包含 K 的键值对
type Omit = Pick<T, Exclude<keyof T, K>>
在定义中,第一步先从 T 的 key 中去掉与 K 重叠的 key,接着使用 Pick 把 T 类型和剩余的 key 组合起来即可 举个例子
type Person = {
name: string,
age: number,
eat: () => number,
work: () => void
}
type Test = Omit<Person, 'name'|'age'>
const a: Test = { eat: ()=>{}, work: ()=>{} }
ReturnType
此工具就是获取 T 类型(函数)对应的返回值类型
type ReturnType<T extends (...args: any) => any>
= T extends (...args: any) => infer R ? R : any;
想获取一个函数的返回值是啥类型,就用这个工具函数
function test(x: string | number): string | number { /*..*/ }
type TestType = ReturnType<test>; // string | number
Required
之前说了个把类型T中的所有属性变为可选,这个正好相反 此工具可以将类型 T 中所有的属性变为必选项
type Required<T> = {
[P in keyof T]-?: T[P]
}
这里有个语法 -?
,可以理解为就是 TS 中把?可选属性减去
tsconfig.json
include 指定一个相对或者绝对文件的列表进行编译,可以使用路径模式匹配
{
"compilerOptions": {},
"include": ["./src/index.ts"] // 此时只会编译这个文件,其他的不会被编译
}
exclude 排除一个相对或者绝对文件的列表进行编译,可以使用路径模式匹配。
// 假设 src 为工作目录
{
"compilerOptions": {},
"exclude": ["./src/index.ts"] // 排除 src 下的 index.ts 文件不进行编译
}
路径配置模式
* 匹配 0 或者 多个字符,不包含目录分隔符 /。
? 匹配任意一个字符,不包含目录分隔符 /。
**/ 递归匹配任意子目录。
{
"exclude": ["./src/**/*"], // src目录下的任意目录下的任意文件
"exclude": ["./src/**/*.log"], // src目录下任意目录下的以 .log 结尾的任意文件
"exclude": ["./src/**/?.ts"] // src目录下任意目录下的以单个字母命名的 ts文件。
}
六、项目实战
Q: 偏好使用 interface 还是 type 来定义类型?
A: 从用法上来说两者本质上没有区别,使用 React 项目做业务开发的话,主要就是用来定义 Props 和state、model类型
但是从扩展的角度来说,type 比 interface 更方便拓展一些,假如有以下两个定义:
type Name = { name: string };
interface IName { name: string };
想要做类型的扩展的话,type 只需要一个&,而 interface 要多写不少代码
type Person = Name & { age: number };
interface IPerson extends IName { age: number };
从命名上来说,Typescript是干啥的嘛,类型限制,类型 = type,那我们平时写的限制,用type,无可厚非吧 interface=接口,它应该是暴露给外部的一个公共通道。java中就有很多interface,都是比较抽象的概念。
Q: 是否允许 any 类型的出现
A: 使用any,等于没有TS限制
Q: 类型定义文件(.d.ts)是干啥的?该怎么写,写在哪个文件夹位置?
A: d.ts是声明文件,里面放的都是type啊、interface啊之类的类型定义。(这个我没深入研究,ts官方文档有讲这个) 在业务开发中,如果有比较公用的类型,可以写在d.ts文件中,像我们平时用的第三方库,它们的声明文件,通常都是写在一个d.ts文件中,一次性暴露出去 至于写在哪个位置,就跟我们写model或者components一样,它属于哪部分,就写在哪里