你知道TypeScript这些姿势么
前话
「TypeScript」赋予了「JavaScript」类型定义的能力,让JS既能保留动态类型编程语言的开发体验,又能享受到静态类型编程语言的编译时严谨的类型检查。
新人刚学TypeScript时,往往熟悉基本的类型定义、接口声明、继承多态和泛型后就上手TS开发了,在项目中简单的限定对象类型或者定义函数输入输出,遇到复杂的类型嫌麻烦直接就粗暴使用「any」,极端情况下甚至把TypeScript项目直接变成了「AnyScript」。
这显然违背了TypeScript的初衷,也未能充分正确使用TypeScript提供的能力。
记住哈,如果你决定使用TypeScript作为项目主开发语言,请尽可能给变量等赋予正确的类型。
本篇仅作为本人学习官方文档后的个人理解,如有出错还望大佬们指出改正。
实际上TypeScript有很多「骚姿势」帮助我们写完整的类型正确的代码,只是我们可能不了解他们,而本篇就简单抛砖引玉,把一些实际项目中用得上的写下来。
下面开始正文。
正文
1. 用 any 还是 unknown ?
TypeScript刚发布时,就提供了「any」类型,一个可变变量如果声明类型为any类型,那么它可以接受「任何类型」的值,如下:
let val: any;
val = 1; // 接受「number」类型变量
val = 'a'; // 接受「string」类型变量
val = () => console.log('hello world'); // 接受「Function」类型变量
但是同样的,因为是「任何值」,因此它做任何事情ts在编译时都认为它是对的,因此容易导致运行时出现异常报错,如下:
let val: any;
val = 1;
val.hello(); // 这里编译时不会报错,但运行时会抛出异常
所以如果想用一个变量接受任何值,但是又不想被「百分百信任」导致写出了bug,我们更好的方式是去使用「unknown」类型。
「unknown」类型跟「any」类型最大的不同是,对一个「unknow」变量,你想使用它做一些事情之前,你得「判断它的类型」或者「断言它的类型」。
function fn(val: unknown) {
val.toLowerCase(); // ts类型检查提示错误,编译时异常
val(); // ts类型检查提示错误,编译时异常
}
1.1 判断它的类型
「判断它的类型」很好理解,代码演示如下:
function fn(val: unknown) {
if (typeof val === 'string') { // 判断为「string」类型
console.log(val.toLowerCase());
} else if (typeof val === 'function') { 判断为「function」类型
val();
}
}
1.2 断言它的类型
「断言」也就是在代码逻辑中强制将「unknown」转换为已知的类型。
这是合理的,因为有些情况我们是确定它的类型的,如下:
let val: unknown;
val = 'hello world';
const str = val as string; // 将val断言为「string」类型
console.log(str);
在「给变量断言」后,ts编译器会「百分百相信」,因此编译时直接通过检查。因此「断言」是个会出错的导致写bug的动作(编译通过,运行时异常),在「断言」过程中需要多加注意。
1.3 小结
到这里读者应该大致了解了「any」和「unknow」的区别,因此项目中如果要使用一个「万能」类型,尽可能偏向于使用「unknown」。
「unknown」对比「any」,「unknown」类型范围更小,更能发挥TypeScript的「类型限定」的能力。
这里不是禁用「any」类型,「any」也是个重要的类型,在某些场合发挥不可替代的作用,例如新人们一开始使用它的初衷——「类型极为复杂的实在难以定义,但是我知道它可以用来XXX」
2. void 和 never 区别?
之所以同时讲解他们是因为它们具有相似之处。
2.1 作为变量类型
作为变量类型的时候他们都为「空」,但是在变量接受上面存在区别,「void」能接受「undefined」值,但是「never」只能接受「never」,代码演示如下:
let a: void;
a = undefined; // 编译正常
let b: never;
b = undefined; // 类型检查报错
let c: never;
c = b; // 编译正常
在正确使用场景上来说「never」类型的变量不存在,通过「enum」类型代码演示如下:
enum EType {
"a" = "a",
"b" = "b",
}
function fn(type: EType) {
switch (type) {
case EType.a:
break;
case EType.b:
break;
default:
// 这里的val变量类型为「never」
// 因为case已经处理了所有枚举值的场景,若代码执行到default应为异常场景
const val = type;
break;
}
}
2.2 作为返回值类型
返回值为「void」的函数实际能返回任何值,但接受返回值的变量类型为「void」。这是合理的,因为接受返回值的变量并不能「做任何事情」,如下:
type IFn = () => void;
const fn1: IFn = () => {
return; // 编译正常
};
const t1 = fn1(); // 值类型为「void」
const fn2: IFn = () => {
return "Hello"; // 编译正常
};
const t2 = fn2(); // 值类型为「void」
console.log(t2.toLowerCase()); // 类型检查报错
返回值为「never」的场景只存在于「死循环」或者「抛出异常」的场景,代码讲解如下:
type IFn = () => never;
/** 死循环函数 **/
const fn1: IFn = () => {
while (true) {
// 编译正常
}
};
/** 抛异常函数 **/
const fn2: IFn = () => {
throw new Error(); // 编译正常
};
上面的代码很好解释了前面说的「never」类型的变量「不存在」。
3. 如何获取类型的子属性类型?
首先说明下这个小标题是什么意思?实际开发中存在这么一个场景,使用一个开源组件库,其导出了一个复杂的数据类型,如下:
export interface IData = {
id: string;
user: {
name: string;
info : {
avatar: string;
}
}
}
如果我们需要预先定义一个变量去接收这个复杂类型中的「user」属性,那这个「user」变量类型该如何定义?
有一个比较low的方法去做,使用「typeof」如下:
import { data } from 'xxx';
let val: typeof data.user; // 使用typeof获取data.user的类型
...
val = data.user; // 接收data子属性user值
最为标准的做法「像使用对象属性一样去定义类型」,代码演示如下:
import { data, IData } from 'xxx';
let val: IData['user']; // 像使用对象属性一样去定义类型
...
val = data.user; // 接收data子属性user值
4. in keyof 有什么用?
「in keyof」用法较多,但最为常用的是「使用它对类型进行二次包装」。例如有如下的表单类型:
type IForm = {
name: string;
age: number;
avatar: string;
}
我要将所有子属性类型包裹一层「value」,如 name: { value: string; }
,这时候就可以使用「in keyof」对其进行包装:
type IFormValue = {
[K in keyof IForm]: {
value: IForm[K];
}
}
配合「获取子属性类型」我们还可以将「IFormValue」类型还原为「IForm」类型,如下:
type IForm = {
[K in keyof IFormValue]: IFormValue[K]['value'];
}
「in keyof」的能力远不止于此,其他用法读者请自行探索哈~
5. 使用 extends 限定泛型类型
「泛型」的强大毋庸置疑,让下面的「函数」得以复用,并且能够得到「准确类型」的返回值:
function fn<T>(val: T) {
console.log(`Hello ${val}`);
return val;
}
fn(1996); // 'Hello 1996'
fn("Pwcong"); // 'Hello Pwcong'
fn({
toString: function () {
return "ByteDance";
},
}); // 'Hello ByteDance'
我们可以传入「number」、「string」甚至一些重写了「toString」实现的「对象」类型,但是一旦我们传入了「期望外」的类型,那这个函数就会打印出奇奇怪怪的日志了。
这时候对于使用「泛型」的函数更好的方式是去使用「extends」对传入的类型进行限定,如下:
function fn<T extends number | string>(val: T) {
console.log(`Hello ${val}`);
return val;
}
fn(1996); // 编译通过
fn("pwcong"); // 编译通过
fn({
toString: function () {
return "ByteDance";
},
}); // 类型检查报错
6. 常见的工具类型
⚠️注意:文章到这里开始简单演示常见的「工具类型」,完整的「工具类型」文档请参考官方文档地址:www.typescriptlang.org/docs/handbo…
6.1 Partial: 选填的子属性
// 「IForm」类型所有属性「必填」
type IForm = {
name: string;
age: number;
avatar: string;
}
type INewForm = {
name?: string;
age?: number;
avatar?: string;
}
// 这个类型跟上面的类型相同,使用「Partial」将其所有属性变为「选填」
type INewForm = Partial<IForm>;
6.2 Record: 自定义对象类型
type IForm = { [key: string]: string | number }
// 这个类型跟上面的类型相同,使用「Record」自定义对象类型
type INewForm = Record<string, string | number>;
const form: INewForm = {
name: 'Pwcong',
age: 18,
avatar: 'https://xxx',
}; // 编译通过
6.3 Pick: 选取类型子属性
type IForm = {
name: string;
age: number;
avatar: string;
}
type INewForm = {
name?: string;
avatar?: string;
}
// 这个类型跟上面的类型相同,使用「Pick」选取类型子属性
type INewForm = Pick<IForm, 'name' | 'avatar'>;
6.4 Omit: 排除类型子属性
type IForm = {
name: string;
age: number;
avatar: string;
}
type INewForm = {
name?: string;
}
// 这个类型跟上面的类型相同,使用「Omit」排除类型子属性
type INewForm = Omit<IForm, 'age' | 'avatar'>;
6.5 Exclude: 排除联合类型
type IType = number | string | boolean
type INewType = number
// 这个类型跟上面的类型相同,使用「Omit」排除类型子属性
type INewType = Exclude<IType, string | boolean>
6.6 Parameters: 获取函数形参的类型
type IFn = (name: string, age: number) => boolean;
type IName = Parameters<IFn>[0]; // 「string」类型
type IAge = Parameters<IFn>[1];// 「number」类型
6.7 ReturnType: 获取函数返回值的类型
type IFn = () => string;
type IRet = ReturnType<IFn>; // 「string」类型
总结
到这里大概过了一些「常见的TypeScript姿势」,希望能够帮助到读者们哈,如有出错还望大佬们指出改正~
转载自:https://juejin.cn/post/7064076065446035487