likes
comments
collection
share

TypeScript技术系列:复杂基础类型详解与应用

作者站长头像
站长
· 阅读数 26

前言

在前面的文章中,我们探讨了TypeScript的基本类型和它们的使用。随着对TypeScript的深入了解,我们会发现一些更加复杂的基础类型,如数组、元组和特殊类型(例如anyunknownvoid等)。这些类型在实际开发中有着广泛的应用和重要性。本篇文章将详细介绍这些复杂的基础类型及其使用场景。

1、数组类型(Array)

TypeScript中,可以像JavaScript一样定义数组,并且可以明确指定数组元素的类型。以下是定义数组类型的两种方式:

1.使用[]的形式:

let numberArray: number[] = [10, 20, 30]; 
let stringArray: string[] = ['apple', 'banana', 'cherry']; 

2.使用Array泛型:

let numberArray: Array<number> = [10, 20, 30]; 
let stringArray: Array<string> = ['apple', 'banana', 'cherry']; 

虽然两种方式没有本质区别,但推荐使用[]形式,因为它更加简洁,并且避免了与JSX语法的冲突。

如果明确指定了数组元素的类型,以下所有操作都将因为不符合类型约定而提示错误。

let numberArray: number[] = ['a', 'b', 'c']; // 提示 ts(2322)
numberArray[3] = 'a'; // 提示 ts(2322)
numberArray.push('b'); // 提示 ts(2345)
let stringArray: string[] = [10, 20, 30]; // 提示 ts(2322)
stringArray[3] = 1; // 提示 ts(2322)
stringArray.push(2); // 提示 ts(2345)

TypeScript技术系列:复杂基础类型详解与应用

2、元组类型(Tuple)

元组是一种可以限定数组元素个数和类型的结构,适合用于多值返回的场景。例如,ReactuseState Hook返回一个元组:

import { useState } from 'react';

function useCounter() {
    const [count, setCount] = useState(0);
    return [count, setCount];
}

TypeScript中,可以明确地定义元组类型:

let tuple: [number, string, boolean] = [42, 'hello', true];

如果交换元组元素的位置,TypeScript会给出错误提示:

let tuple: [number, string, boolean] = ['hello', 42, true]; // Error

TypeScript技术系列:复杂基础类型详解与应用

接下来将介绍几种不一样且需要费点心力理解的类型——特殊类型

3、特殊类型

3.1 any

any指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式。

可以对被注解为any类型的变量进行任何操作,包括获取事实上并不存在的属性、方法,并且TypeScript还无法检测其属性是否存在、类型是否正确。

比如可以把任何类型的值赋值给any类型的变量,也可以把any类型的值赋值给任意类型(除 never 以外)的变量,如下代码所示:

let obj: any = {};
obj.doAnything(); // 不会提示错误
obj = 1; // 不会提示错误
obj = 'x'; // 不会提示错误
let num: number = obj; // 不会提示错误
let str: string = obj; // 不会提示错误

如果不想花费过高的成本为复杂的数据添加类型注解,或者已经引入了缺少类型注解的第三方组件库,这时就可以把这些值全部注解为any类型,并告诉TypeScript选择性地忽略静态类型检测。

尤其是在将一个基于JavaScript的应用改造成TypeScript的过程中,不得不借助any来选择性添加和忽略对某些JavaScript模块的静态类型检测,直至逐步替换掉所有的JavaScript

any类型会在对象的调用链中进行传导,即所有any类型的任意属性的类型都是any,如下代码所示:

let obj: any = {};
let z = obj.a.b.c; // z 类型是 any,不会提示错误
z(); // 不会提示错误

这里需要明白且记住:Any is Hell(Any 是地狱)

从长远来看,使用any绝对是一个坏习惯。如果一个TypeScript应用中充满了any,此时静态类型检测基本起不到任何作用,也就是说与直接使用JavaScript没有任何区别。因此,除非有充足的理由,否则应该尽量避免使用any,并且开启禁用隐式any的设置。

3.2 unknown

unknownTypeScript 3.0中添加的一个类型,它主要用来描述类型并不确定的变量。

比如在多个if else条件分支场景下,它可以用来接收不同条件下类型各异的返回值的临时变量,如下代码所示:

let res: unknown;
if (x) {
  res = x();
} else if (y) {
  res = y();
} ...

3.0以前的版本中,只有使用any才能满足这种动态类型场景。

any不同的是,unknown在类型上更安全。比如可以将任意类型的值赋值给unknown,但unknown类型的值只能赋值给unknownany,如下代码所示:

let res: unknown;
let num: number = res; // 提示 ts(2322)
let anything: any = res; // 不会提示错误

TypeScript技术系列:复杂基础类型详解与应用

使用unknown后,TypeScript会对它做类型检测。但是,如果不缩小类型,对unknown执行的任何操作都会出现如下所示错误:

let res: unknown;
res.toFixed(); // 提示 ts(2339)

TypeScript技术系列:复杂基础类型详解与应用

而所有的类型缩小手段对unknown都有效,如下代码所示:

let res: unknown;
if (typeof res === 'number') {
  result.toFixed(); // 此处 hover result 提示类型是 number,不会提示错误
}

3.3 void、undefined、null

考虑再三,还是决定把voidundefinednull“三废柴”特殊类型整合到一起介绍。

首先来说一下void类型,它仅适用于表示没有返回值的函数。即如果该函数没有返回值,那它的类型就是void

strict模式下,声明一个void类型的变量几乎没有任何实际用处,因为不能把void类型的变量值再赋值给除了anyunkown之外的任何类型变量。

然后说说undefined类型和null类型,它们是TypeScript值与类型关键字同名的唯二例外。但这并不影响它们被称为“废柴”,因为单纯声明undefined或者null类型的变量也是无比鸡肋,示例如下所示:

let undef: undefined = undefined;
let nul: null = null; 

undefined的最大价值主要体现在接口类型上,它表示一个可缺省、未定义的属性。

null的价值我认为主要体现在接口制定上,它表明对象或属性可能是空值。尤其是在前后端交互的接口,比如任何涉及查询的属性、对象都可能是null空对象,如下代码所示:

const userInfo: {name: null | string} = { name: null };

除此之外,undefinednull类型还具备警示意义,它们可以提醒我们针对可能操作这两种(类型)值的情况做容错处理。

需要类型守卫在操作之前判断值的类型是否支持当前的操作。类型守卫既能通过类型缩小影响TypeScript的类型检测,也能保障JavaScript运行时的安全性,如下代码所示:

const userInfo: { id?: number; name?: null | string } = { id: 1, name: 'maybe' };
if (userInfo.id !== undefined) { 
  userInfo.id.toFixed(); // id 的类型缩小成 number
}

不建议随意使用非空断言来排除值可能为nullundefined的情况,因为这样很不安全。

userInfo.id!.toFixed(); // ok,但不建议
userInfo.name!.toLowerCase() // ok,但不建议

而比非空断言更安全、类型守卫更方便的做法是使用单问号(Optional Chain)双问号(空值合并),可以使用它们来保障代码的安全性,如下代码所示:

userInfo.id?.toFixed(); // Optional Chain
const myName = userInfo.name?? `my name is ${info.name}`; // 空值合并

3.4 never

never表示永远不会发生值的类型,这里举一个实际的场景进行说明。

首先,定义一个统一抛出错误的函数,代码示例如下:

function ThrowError(msg: string): never {
  throw Error(msg);
}

以上函数因为永远不会有返回值,所以它的返回值类型就是never

同样,如果函数代码中是一个死循环,那么这个函数的返回值类型也是never,如下代码所示。

function Loop(): never {
  while (true) {}
}

never是所有类型的子类型,它可以给所有类型赋值,如下代码所示。

let Nev: never = 1; // ts(2322)
Nev = 'string'; // ts(2322)
Nev = true; // ts(2322)
let num: number = Nev; // ok
let str: string = Nev; // ok
let bool: boolean = Nev; // ok

TypeScript技术系列:复杂基础类型详解与应用

但是反过来,除了never自身以外,其他类型(包括any在内的类型)都不能为never类型赋值。

在恒为false的类型守卫条件判断下,变量的类型将缩小为nevernever是所有其他类型的子类型,所以是类型缩小为never,而不是变成never)。因此,条件判断中的相关操作始终会报无法更正的错误,如下代码所示:

const str: string = 'string';
if (typeof str === 'number') {
  str.toLowerCase(); 
}

基于never的特性,还可以使用never实现一些有意思的功能。比如可以把never作为接口类型下的属性类型,用来禁止写接口下特定的属性,示例代码如下:

const obj: { id: number, name?: never } = {
  id: 1
}
obj.name = null; // ts(2322))
obj.name = 'str'; // ts(2322)
obj.name = 1; // ts(2322)

TypeScript技术系列:复杂基础类型详解与应用 此时,无论给obj.name赋什么类型的值,它都会提示类型错误,实际效果等同于name只读 。

3.5 object

object类型表示非原始类型的类型,即非numberstringbooleanbigintsymbolnullundefined的类型。然而,它也是个没有什么用武之地的类型,如下所示的一个应用场景是用来表示Object.create的类型。

declare function create(o: object | null): any;
create({}); // ok
create(() => null); // ok
create(2); // ts(2345)
create('string'); // ts(2345)

4、类型断言(Type Assertion)

TypeScript类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到比TypeScript更清楚实际类型的情况,比如下面的例子:

const arr: number[] = [10, 20, 30, 40];
const res: number = arr.find(num => num > 20); // 提示 ts(2322)

TypeScript技术系列:复杂基础类型详解与应用 其中,res一定是一个数字(确切地讲是30),因为arr中明显有大于20的成员,但静态类型对运行时的逻辑无能为力。

TypeScript看来,res的类型既可能是数字,也可能是undefined,所以上面的示例中提示了一个ts(2322)错误,此时我们不能把类型undefined分配给类型number

不过,可以使用一种笃定的方式——类型断言告诉TypeScript按照我们的方式做类型检查。

比如,可以使用as语法做类型断言,如下代码所示:

const arr: number[] = [10, 20, 30, 40];
const res: number = arr.find(num => num > 20) as number;

又或者是使用尖括号+类型的格式做类型断言,如下代码所示:

const arr: number[] = [10, 20, 30, 40];
const res: number = <number>arr.find(num => num > 20);

以上两种方式虽然没有任何区别,但是尖括号格式会与JSX产生语法冲突,因此更推荐使用as语法。

此外还有一种特殊非空断言,即在值(变量、属性)的后边添加 '!' 断言操作符,它可以用来排除值为nullundefined的情况,具体示例如下:

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2533)

TypeScript技术系列:复杂基础类型详解与应用 对于非空断言来说,同样应该把它视作和any一样危险的选择。

在复杂应用场景中,如果使用非空断言,就无法保证之前一定非空的值。而一旦保证被改变,错误只会在运行环境中抛出,而静态类型检测是发现不了这些错误的。

所以,建议使用类型守卫来代替非空断言,比如如下所示的条件判断:

let mayNullOrUndefinedOrString: null | undefined | string;
if (typeof mayNullOrUndefinedOrString === 'string') {
  mayNullOrUndefinedOrString.toString(); // ok
}

到这里,TypeScript所有的基础类型就交代完了。

总结

在这篇文章中,我们探讨了TypeScript中的一些复杂基础类型,包括数组、元组以及一些特殊类型。理解和正确使用这些类型,可以提升代码的可读性和安全性。在接下来的文章中,我们将继续深入探讨TypeScript的更多高级特性和应用场景,敬请期待!

后语

小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走吧^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

转载自:https://juejin.cn/post/7394345043132645402
评论
请登录