背景
本文记录下在学习 Typescript 过程中的一些重点
基本知识
基本类型
Typescript 中有以下几种基本的数据类型: boolean, number, string, 数组, 元组, enum, any, void, undefined, null, never, object
// 布尔
let isDone: boolean = false;
// 数字
let decLiteral: number = 6;
// 字符串
let name: string = "bob";
// 数组
let list: number[] = [1, 2, 3];
// 元组, 其实就是数组中的元素支持不同的类型
let x: [string, number];
x = ['hello', 10];
// 枚举
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
// any
let notSure: any = 4;
// void, 表示没有任何的类型, 一般用于函数没有返回值的情况
function warnUser(): void {
console.log("This is my warning message");
}
// undefined, null
let u: undefined = undefined;
let n: null = null;
// never, 表示永不存在的类型
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// object, 表示非原始类型, 一般用的很少
高级类型
接口
接口应该是最常用的功能之一了,你可以将多个不同的类型组合在一个接口类型中,用来表达复杂数据类型,使用示例如下:
interface ILabelledValue {
label: string; // 包含 label 属性,并且类型为 string
color?: string; // 可能包含 color,也可能不包含 color 属性
[propName: string]: any; // 其余属性的类型均为 any
}
第一个属性 label 表示该类型中一定有 label 这个字段,并且类型为 string,如下:我们传入一个包含其他字段的值进去时,ts 会直接报错
第二个 color 属性后面带一个 ? 表示该属性可能存在也可能不存在,如下:传入的参数中既可以包含 color,也可以不包含 color,同时在变量上可以获取到 color 属性
第三个表示其他任何属性的类型为 any,如下:这时候我们可以给改类型的变量传递任何的属性,类型均为 any,但需要注意,这里的 label 属性依然是必须的
额外的属性检查
现在想象一下这样一个场景:当前的接口类型中只有一个属性 label,此时如果传入的参数中包含有其他属性,就会报错,如下:这个也是上面说到的,如果要修复这个问题的话,可以通过在接口类型中添加 "[propName: string]: any "这个声明来解决这个问题,但添加这个声明会导致原先的接口类型支持所有类型的属性,等于把它原先的限制范围给打破了,并不是一个好的方式
针对上述场景,我们有两个方法,一种是这里直接添加断言,如下:此时就只会检查传入的值中是否包含了接口中定义的属性,而对于其他不在接口中的属性则不检查
另一种方式就是使用一种绕过严格检查的方式,具体的做法是先将值赋值给一个变量,然后再将该变量作为参数传入即可,这时候 ts 中不会进行严格的类型校验,只要传入的值中存在对应属性即可,多余的属性不会进行校验,如下,也能达到不检查其余属性的目的(这种方式一般不推荐)
函数类型
既可以直接在函数中添加参数、返回值等对应的类型,也可以单独声明一个函数类型,如下:一般第一种比较常用
// 直接在函数参数、返回值中声明类型
function add(x: number, y: number): number {
return x + y;
}
// 专门声明一个函数类型, 注意:函数类型中的参数命名和真实的参数名可不一致
const myAdd: (x: number, y: number) => number = function(x1: number, y1: number): number {
return x + y;
};
// 通过接口来专门声明一个函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
函数中也可以添加可选参数,和接口中的可选属性一致,通过"?"来表示,如下:需要注意的是,可选属性、默认值属性都是需要放在参数列表的最后的
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + " " + lastName;
}
else {
return firstName;
}
}
泛型
有时候我们的函数或者组件需要考虑可重用性,不仅需要支持目前的数据类型,还要支持未来不确定的类型,这时候我们就可以使用泛型了
我们先看一个例子,假设我们现在需要开发一个函数,它的功能很简单,就直接返回参数即可,如下。这里我们需要考虑下函数的参数类型和返回类型。假如我们知道了入参为 number,那么我们返回值就是 number,那如果我们现在不知道参数的类型呢,因为我们这个函数是要能支持未来不确定的类型的,所以参数不应该预设
你可能会想到使用 any 类型,就是输入和返回都是用 any 类型,这么做看似可以,但其实缺少了一些信息:函数的参数类型和返回类型应该是一致的。所以如果都使用 any 类型,则无法保证参数和返回值的类型一致
function identity1(arg: number): number {
return arg;
}
function identity2(arg: any): any {
return arg;
}
为了解决上面的问题,这里我们就需要一种表示类型的变量,也就是泛型,如下:这里我们使用<T>来表示一个类型变量,下面代码表示这个函数的参数类型为T,这个类型可以在实际调用函数的时候再确定
function identity<T>(arg: T): T {
return arg;
}
// 可以通过在 <> 中传入类型来调用
let output = identity<string>("myString");
// 也可以直接调用,ts 会自动推导该类型
let output2 = identity("myString");
当然,你可以在函数中任意使用该变量类型,如下:
function identity<T>(arg: T): T {
const temp: T[] = []; // 可以任意使用该范型 T
return arg;
}
带有这种泛型的函数的声明方式也比较简单,就在原先的声明方式前添加<T>即可:
function identity<T>(arg: T): T {
return arg;
}
// 直接声明泛型函数类型
let myIdentity: <U>(arg: U) => U = identity;
// 接口的方式声明泛型函数
interface GenericIdentityFn {
<T>(arg: T): T;
}
可以支持多个泛型变量,如下:
function identity<T, U>(arg1: T, arg2: U): T {
return arg1;
}
const ret = identity<string, number>('', 0);
接口类型也支持泛型,如下:
interface ILabel<T> {
label: T;
value: T[];
}
const msg: ILabel<string> = {label: '', value: ['']};
const ts: ILabel<number> = {label: 0, value: [1, 2, 3]}
泛型约束
上面讲了泛型的用法,我们知道了泛型是一种类型的变量,我们可以将任何类型都传给他,但如果我们想对这个类型做一些约束呢,比如虽然我不确定实际的类型,但我需要限制传入的参数一定要有length 属性,这种情况就是“泛型约束”
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity例子中,我们想访问arg的length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
这个时候我们可以约束下这个类型,使传入的参数必须包含 length 属性,如下:我们先定义一个需要的接口类型,然后通过 extends 来约束该泛型
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
除了约束一个类型参数外,我们还可以用一个类型来约束另一个类型,如下:这里声明了两个类型变量 Type 和 Key,同时约束 Key 为 Type 的属性
keyof 操作
keyof 返回一个类型的所有属性
type Point = { x: number; y: number };
type P = keyof Point; // P 的类型等于 “x” | “y”
typeof 操作
typeof 操作可以返回一个变量的类型,这个操作对一些简单的基本类型没有多大帮助,但可以很方便的获取一些复杂类型,如下:类型 P 就是函数 f 的返回类型
交叉类型
交叉类型是将多个类型合并为一个类型,如下:其中 m 变量是 {a: number} 和 {b: number} 这两个类型的交叉类型,所以 m 的值中既有 a 属性,又存在 b 属性
const m: { a: number } & { b: string } = {
a: 123,
b: 'hello world'
}
联合类型
顾名思义,可以将不同的类型联合起来,组成一个新的类型。新类型可以是组成类型中的任意一个,如下:
const msg: string | number = 0; // 联合类型,可以赋值为其中任何一个类型
function print(msg: string | number): void {
console.log(msg.length); // error: 这里 msg 既可以是 string,也可以是 number,所以直接调用会报错
}
function print2(msg: string | number): void {
if (typeof msg === 'string') { // 提前判断 msg 的类型
console.log(msg.length); // success
}
console.log((msg as string).length); // 我们也可以使用类型断言
}
在实际业务中,我们经常会遇到字段为 undefined 或者 null 等情况,这时候我们经常需要使用联合类型来拓展我们原先的类型,如下:我们可以先对类型进行判断,排除 null 这个情况,但这种方式会比较麻烦
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
所以我们也经常会使用短路运算,如下:这样就可以兼容 null 的情况
function f(sn: string | null): string {
return sn || "default";
}
此外,ts 中还提供了 "!" 可以明确告诉编译器,此时已经没有 null 的情况了,相当于类型断言,如下:
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
}
name = name || "Bob"; // 这里虽然用到了短路运算,但在后续运行的 postfix 函数中编译器没有识别出来
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
类型别名
类型别名本质是给现有的类型起一个新的名字,它并不会创建一个新的类型,只是对这个类型创建了一个新的引用,如下:
type Name = string;
function getName(n: Name): Name {
// ...
}
类型别名也可以是泛型,如下:
type Container<T> = { value: T };
类型推导
基础
在 ts 中,有时候不需要手动去声明变量的类型,一些基本简单的类型可以在赋值的时候被自动推导出来,如下:变量 x 被赋值为 0 时,会自动推导为 number 类型,后续监测到类型不一致时,则会报错
最佳通用类型
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如:
let x = [0, 1, null];
为了推断x的类型,我们必须考虑所有元素的类型。这里有两种选择:number 和 null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型
类型断言
在有些情况下,你比编译器更明白这个变量的类型,这时候就可以手动告诉编译器这个变量的确定类型,如下:下面这个变量 msg 的类型是 string 或者是 number,但我们使用的时候直接当作 string 类型来使用了,这时候编译器就会报错,因为对于 number 类型并没有这个方法
但假如我们知道在 do something 后,msg 的变量类型肯定是 string 类型,这时候就可以使用断言来告诉编译器他的准确类型,如下:使用 "as" 来完成断言操作,这时候编译器就不会报错了
当然,现在的编译器已经很智能了,有时候它自己也会进行一些基本的断言,如下:假如我们代码前面做了一些处理,在 number 类型时就直接返回了,那么编译器也能自己推断出后续 msg 的类型只能是 string,所以这里也不会报错
环境声明
环境声明可以允许你安全的使用现有的各种 javascript 库。可以通过 declare 关键字来告诉 TypeScript,你正在试图表述一个其他地方已经存在的代码,如:下面的例子中是业务中使用 arms 的 sdk 来更新一个配置,在页面初始化的时候,sdk 已经初始化,并将 __bl 变量挂载在 window 全局对象上,但 ts 并不知道,所以这里会报错
为了解决上述问题,我们需要在 global.d.ts 文件中去声明这个全局变量,如下:
最佳实践
接口请求
接口应该是我们平时开发中最常接触到的,也是最需要类型的去约束的。实际业务中我们使用 @alife/whale-request 请求库进行请求,但该函数不支持泛型参数,因此我们这里使用类型推断的方式来指定函数的返回值,如下:
import request from '@alife/whale-request';
interface IUserParam {
workNo: string;
}
export interface IUser {
workNo: string;
nickName: string;
deptName: string;
grantType: string;
itemCode: string;
itemName: string;
grantTypeName: string;
}
export async function getUserInfo(params: IUserParam): Promise<IUser> {
return request({
url: '/api/getUser',
data: params
}) as Promise<IUser>
}
函数的使用如下,注意:这里 ts 会自动将变量的类型推导为函数的返回值类型,所以不需要给 ret 变量手动指定类型了,然后在使用的时候,编译器会自动将其中的属性展示出来
同时,因为我们严格的指定了函数的输入参数类型、返回类型,所以当我们输入给函数的参数和其类型不匹配时,会高亮出来提示我们参数不匹配,这时候我们可以查看下这里的传参是否有问题等,能帮助我们发现一些潜在的问题(比如传入的参数拼写错误,或者这个函数被其他地方调用了,但参数的格式并没有保持一致等问题)
Hooks 中 ts 的使用
我们在上述例子的基础上来说明下在一般的 Hooks 中是如何使用 ts 的类型。这里我们封装一个能获取用户基本信息的 Hooks,同时还包含数据请求时的 loading 状态,以及重新加载信息的函数。如下,其中 useState 是支持泛型的,我们可以传入指定的类型。注意:这里的hooks 的返回值是有必要特地指定的,因为返回的列表中的三个元素的类型各不相同,否则 ts 会自动推导为这三个类型的最大公共类型
type useLoadUserInfoReturnType = [IUser, boolean, () => Promise<void>];
function useLoadUserInfo(workNo: string): useLoadUserInfoReturnType {
const [userInfo, setUserInfo] = useState<IUser>();
const [loading, setLoading] = useState(false);
const reload = async () => {
setLoading(true);
const ret = await getUserInfo({workNo});
setUserInfo(ret);
setLoading(false);
}
useEffect(() => {
reload();
}, [workNo])
return [userInfo, loading, reload];
}
上述 hook 的使用如下,我们可以看到 ts 中可以正确获取返回的类型
组件中的 ts 使用
在组件的开发中,我们可以指定组件接收的属性,以及对应的类型,帮助我们更佳方便和正确的使用组件。在实际业务中,我们基本都使用 Function 组件,而 React 给我们提供了 FC 类型来约束 Function 组件,如下:
interface IUserCardProps {
workNo: string;
}
const UserCard: React.FC<IUserCardProps> = ({ workNo }) => {
const [userInfo, loading, reload] = useLoadUserInfo(workNo);
return (
<div>
name: {userInfo.deptName}
</div>
)
}
使用如下,我们可以正确约束组件的属性,并将使用错误的情况高亮出来,供我们检查: