TypeScript入门实战笔记 -- 03 复杂基础类型:TypeScript 与 JavaScript 有何不同?

发布时间:2023年12月17日

🍍数组

因为 TypeScript 的数组和元组转译为 JavaScript 后都是数组,所以这里我们把数组和元组这两个类型整合到一起介绍,便于理解。

数组类型(Array)

在 TypeScript 中,我们也可以像 JavaScript 一样定义数组类型,并且指定数组元素的类型。

首先,我们可以直接使用 [] 的形式定义数组类型,如下代码所示:

/** 子元素是数字类型的数组 */
let arrayOfNumber: number[] = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arrayOfString: string[] = ['x', 'y', 'z'];

同样,我们也可以使用 Array 泛型定义数组类型,如下代码所示:

/** 子元素是数字类型的数组 */
let arrayOfNumber: Array<number> = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arrayOfString: Array<string> = ['x', 'y', 'z'];

以上两种定义数组类型的方式虽然本质上没有任何区别,推荐使用 [] 这种形式来定义。一方面可以避免与 JSX 的语法冲突,另一方面可以减少不少代码量

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

let arrayOfNumber: number[] = ['x', 'y', 'z']; // 提示 ts(2322)
arrayOfNumber[3] = 'a'; // 提示 ts(2322)
arrayOfNumber.push('b'); // 提示 ts(2345)
let arrayOfString: string[] = [1, 2, 3]; // 提示 ts(2322)
arrayOfString[3] = 1; // 提示 ts(2322)
arrayOfString.push(2); // 提示 ts(2345)

元组类型(Tuple)

TypeScript 中的元组类型有以下优点:

  1. 混合类型存储:元组允许在同一个集合中存储不同类型的值,这是数组无法满足的需求,因为数组通常只存储相同类型的值。
  2. 固定数量和顺序:元组用于存储固定数量、不同类型的元素。这意味着你可以确保元素的类型和顺序始终保持一致。
  3. 函数参数:元组可以作为参数传递给函数。这使得你可以将一组有序的值作为一个整体传递。
  4. 解构赋值:你可以使用解构赋值来从元组中提取值。这使得你可以方便地将元组元素赋值给变量。
  5. 支持 Rest 参数和 Spread 表达式:从 TypeScript 3.0 开始,TypeScript 支持函数 Rest 参数和 Spread 表达式使用元组类型。
  6. 支持可选元素和 Rest 元素:元组类型允许在元素类型上后缀一个 ? 来指定元素是可选的,同时也支持 Rest 元素。

元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。示例:

// 定义一个元组
let complexTuple: [string, number, boolean] = ['Runoob', 10, true];

// 访问元组元素
console.log(complexTuple[0]); // 输出 'Runoob'
console.log(complexTuple[1]); // 输出 10
console.log(complexTuple[2]); // 输出 true

// 更新元组元素
complexTuple[0] = 'Ws';
console.log(complexTuple[0]); // 输出 'Ws'

// 添加元素到元组
complexTuple.push(false);
console.log(complexTuple); // 输出 ['Bing', 10, true, false]

// 删除元组的最后一个元素
complexTuple.pop();
console.log(complexTuple); // 输出 ['Bing', 10, true]

// 元组解构
let [name, age, isStudent] = complexTuple;
console.log(name); // 输出 'Bing'
console.log(age); // 输出 10
console.log(isStudent); // 输出 true

上述代码执行环境配置:

1: tsc 03.Basic.2.ts --strict --alwaysStrict false --watch ( 执行转换为js操作 )
使用TypeScript编译器TSC来编译并监视03.Basic.2.ts文件的变化,同时启用所有的严格类型检查选项,但在编译后的JavaScript文件中不会强制使用use strict。

2:安装nodemon( 全局安装npm install -g nodemon ) 检测.js文件变化重启项目,打印输出结果

🍍特殊类型

1. any

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

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

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

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

如果我们不想花费过高的成本为复杂的数据添加类型注解,或者已经引入了缺少类型注解的第三方组件库,这时就可以把这些值全部注解为 any 类型,并告诉 TypeScript 选择性地忽略静态类型检测。尤其是在将一个基于 JavaScript 的应用改造成 TypeScript 的过程中,我们不得不借助 any 来选择性添加和忽略对某些 JavaScript 模块的静态类型检测,直至逐步替换掉所有的 JavaScript。any 类型会在对象的调用链中进行传导,即所有 any 类型的任意属性的类型都是 any,如下代码所示:

let anything: any = {};
let z = anything.x.y.z; // z 类型是 any,不会提示错误
z(); // 不会提示错误

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

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

2. unknown

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

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

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

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

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

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

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

let result: unknown;
result.toFixed(); // 提示 ts(2571)

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

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

3. void、undefined、null

在TypeScript中,voidundefinednull是三种不同的类型,它们的区别如下:

  • void:在TypeScript中,void表示该函数没有返回值。例如:
function noReturn(): void {
  console.log("此函数没有返回值");
}
  • undefinedundefined是一个全局变量,表示未定义或未赋值3。如果变量声明了但未初始化,则该变量的默认值为undefined。在函数中,如果你已声明但未返回任何值,则默认返回undefined
  • nullnull是一个表示无值或空值的JavaScript原始值。null意味着对象不包含任何值。如果要显式设置变量或属性不含任何值,可以将其设置为null

在TypeScript中,nullundefined是所有类型的子类型,即可以赋值给任意类型1。但当我们在tsconfig.js文件中设置strictNullCheckstrue时,就不能将nullundefined赋值给除它们自身和void之外的任意类型了。

总的来说,voidundefinednull在TypeScript中有着不同的用途和语义,它们在编程中起着重要的作用。

如何在TypeScript中检查变量是否为null或undefined?

在TypeScript中,你可以使用以下几种方法来检查一个变量是否为nullundefined

  1. 使用=====运算符:
if (variable == null) {
  // 当变量为null或undefined时,这里的代码会被执行
}
  1. 使用typeof运算符:
if (typeof variable === 'undefined') {
  // 当变量为undefined时,这里的代码会被执行
}
  1. 使用逻辑运算符:
if (!variable) {
  // 当变量为null、undefined、NaN、空字符串''、0或false时,这里的代码会被执行
}

4. never

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

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

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

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

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

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

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

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

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

在恒为 false 的类型守卫条件判断下,变量的类型将缩小为 never(never 是所有其他类型的子类型,所以是类型缩小为 never,而不是变成 never)。因此,条件判断中的相关操作始终会报无法更正的错误(我们可以把这理解为一种基于静态类型检测的 Dead Code 检测机制),如下代码所示:

const str: string = 'string';
if (typeof str === 'number') {
  str.toLowerCase(); // Property 'toLowerCase' does not exist on type 'never'.ts(2339)
}

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

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

此时,无论我们给 props.name 赋什么类型的值,它都会提示类型错误,实际效果等同于 name 只读 。

5. object

object 类型表示非原始类型的类型,即非?number、string、boolean、bigint、symbol、null、undefined 的类型。然而,它也是个没有什么用武之地的类型,如下所示的一个应用场景是用来表示 Object.create 的类型。

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

🍍类型断言(Type Assertion)

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

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)

其中,greaterThan2 一定是一个数字(确切地讲是 3),因为 arrayNumber 中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。

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

不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。

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

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;

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

const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = <number>arrayNumber.find(num => num > 2);

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

注意:类型断言的操作对象必须满足某些约束关系,否则我们将得到一个 ts(2352) 错误,即从类型“源类型”到类型“目标类型”的转换是错误的,因为这两种类型不能充分重叠。

另外,在TypeScript中,anyunknown是两种特殊的类型。

  • any类型:any类型可以被赋予任何类型的值,也可以被断言为任何类型。这意味着,当一个变量被定义为any类型时,你可以对它进行任何操作,而不会有类型检查错误。但是,这样做会放弃TypeScript的类型安全性,可能会导致运行时错误。
  • unknown类型:unknown类型也可以被赋予任何类型的值,但在对其进行操作之前,必须先进行类型检查或类型断言。这意味着,你不能直接对unknown类型的变量进行操作,除非你确定了它的具体类型。

因此,这句话的意思是,anyunknown类型在TypeScript中非常灵活,它们可以被赋予任何类型的值,也可以被断言为任何类型。但是,any类型在使用时需要小心,因为它放弃了类型检查,而unknown类型在使用时则需要进行额外的类型检查或类型断言,以确保类型安全。

我们除了可以把特定类型断言成符合约束添加的其他类型之外,还可以使用“字面量值 + as const”语法结构进行常量断言,具体示例如下所示:

/** str 类型是 '"str"' */
let str = 'str' as const;
/** readOnlyArr 类型是 'readonly [0, 1]' */
const readOnlyArr = [0, 1] as const;

在TypeScript中,anyunknown是两种特殊的类型。

  • any类型:any类型可以被赋予任何类型的值,也可以被断言为任何类型。这意味着,当一个变量被定义为any类型时,你可以对它进行任何操作,而不会有类型检查错误。但是,这样做会放弃TypeScript的类型安全性,可能会导致运行时错误。
  • unknown类型:unknown类型也可以被赋予任何类型的值,但在对其进行操作之前,必须先进行类型检查或类型断言。这意味着,你不能直接对unknown类型的变量进行操作,除非你确定了它的具体类型。

因此,这句话的意思是,anyunknown类型在TypeScript中非常灵活,它们可以被赋予任何类型的值,也可以被断言为任何类型。但是,any类型在使用时需要小心,因为它放弃了类型检查,而unknown类型在使用时则需要进行额外的类型检查或类型断言,以确保类型安全。

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

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

对于非空断言来说,我们同样应该把它视作和 any 一样危险的选择。

在复杂应用场景中,如果我们使用非空断言,就无法保证之前一定非空的值,比如页面中一定存在 id 为 feedback 的元素,数组中一定有满足 > 2 条件的数字,这些都不会被其他人改变。而一旦保证被改变,错误只会在运行环境中抛出,而静态类型检测是发现不了这些错误的。

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
文章来源:https://blog.csdn.net/weixin_44412840/article/details/134971313
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。