查看原文
其他

TypeScript 入门指南

CUGGZ 前端充电宝 2022-07-21

新系列 深入浅出TypeScript 来了,本系列至少20+篇。本文为第一篇,来介绍一下TypeScript 以及常见的类型。

一、为什么学习TypeScript?

TypeScript是一门由微软推出的开源的、跨平台的编程语言。它是JavaScript的超集,扩展了 JavaScript 的语法,最终会被编译为JavaScript代码。

TypeScript的主要特性:

  • 超集 :TypeScript 是 JavaScript 的超集;
  • 类型系统:TypeScript在JavaScript的基础上,包装了类型机制,使其变身为静态类型语言;
  • 编辑器功能 :增强了编辑器和IDE功能,包括代码补全、接口提示、跳转到定义、重构等;
  • 错误提示 :可以在编译阶段就发现大部分错误,帮助调试程序。

TypeScript 主要是为了实现以下两个目标:

  • 为JavaScript提供可选的类型系统;
  • 兼容当前以及未来的JavaScript 的特性。

下面就来看看这两个目标是如何实现的。

1. TypeScript 的类型系统

为什么要给JavaScript加上类型呢?

我们知道,JavaScript是一种轻量级的解释性脚本语言。也是弱类型、动态类型语言,允许隐式转换,只有运行时才能确定变量的类型。正是因为在运行时才能确定变量的类型,JavaScript代码很多错误在运行时才能发现。TypeScript在JavaScript的基础上,包装了类型机制,使其变身成为静态类型语言。在 TypeScript 中,不仅可以轻易复用 JavaScript 的代码、最新特性,还能使用可选的静态类型进行检查报错,使得编写的代码更健壮、更易于维护。

下面是 JavaScript 项目中最常见的十大错误,如果使用 TypeScript,那么在编写阶段就可以发现并解决很多 JavaScript 错误了:

类型系统能够提高代码的质量和可维护性,经过不断的实践,以下两点尤其需要注意:

  • 类型有利于代码的重构,它有利于编译器在编译时而不是运行时捕获错误;
  • 类型是出色的文档形式之一,函数签名是一个定理,而函数体是具体的实现。

可以认为,在所有操作符之前,TypeScript 都能检测到接收的类型(在代码运行时,操作符接收的是实际数据;在静态检测时,操作符接收的则是类型)是否被当前操作符所支持。当 TypeScript 类型检测能力覆盖到所有代码后,任意破坏约定的改动都能被自动检测出来,并提出类型错误。因此,可以放心地修改、重构业务逻辑,而不用担忧因为考虑不周而犯下低级错误。

在一些语言中,类型总是有一些不必要的复杂的存在方式,而 TypeScript 尽可能地降低了使用门槛,它是通过如下方式来实现的。

(1)JavaScript 即 TypeScript

TypeScript 与 JavaScript 本质并无区别,我们可以将 TypeScipt 理解为是一个添加了类型注解的 JavaScript,为JavaScript代码提供了编译时的类型安全。

实际上,TypeScript 是一门“中间语言”,因为它最终会转化为JavaScript,再交给浏览器解释、执行。不过 TypeScript 并不会破坏 JavaScript 原有的体系,只是在 JavaScript 的基础上进行了扩展。

准确的说,TypeScript 只是将JavaScript中的方法进行了标准化处理:

  • TypeScript只提供一种新的语法,并不会帮我们解决BUG;
  • TypeScript是一种新的语言,它使我们远离运行时。

(2)类型可以是隐式的

TypeScript 会尽可能的去推断类型信息,以便在开发过程中以更小的成本为我们提供类型安全。通俗来讲就是,即使我们不去显式的声明变量的类型,TypeScript也会自动判断数据类型。比如下面的代码:

let a = 1
a = "996"

这段代码在TypeScript中就会报错,因为TS会知道a是一个数字类型,不能将其他类型的值赋值给a,这种类型的推断是很有必要的。

(3)类型可以是显式的

上面说了,TypeScript会尽可能安全的推断类型。我们也可以使用类型注释,以实现以下两件事:

  • 帮助编译器快速判断变量类型;
  • 强制编译器编译我们认为该去编译的内容,这可以让编译器对代码所做的算法分析与我们对代码的理解所匹配。

(4)类型是结构化的

在一些语言中,类型总是有一些不必要的复杂的存在方式,而 TypeScript 的类型是结构化的。比如下面的例子中,函数会接受它所期望的参数:

interface Point2D {
 x: number;
  y: number;
}

interface Point3D {
 x: number;
  y: number;
  z: number;
}

const point2D: Point2D = {x: 10, y: 10}
const point3D: Point3D = {x: 20, y: 20, z: 20}

function takePoint2D(point: Point2D){}

takePoint2D(point2D)  // 正确,完全匹配
takePoint2D(point3D)  // 正确,可以有额外的信息
takePoint2D({x: 30})  // 错误,缺少y

(5)类型错误不会影响JS运行

为了便于把 JavaScript 代码迁移至 TypeScript,即使存在编译错误,在默认的情况下,TypeScript 也会尽可能的被编译为 JavaScript 代码。因此,我们可以将JavaScript代码逐步迁移至 TypeScript。

(6)类型可以由环境来定义

TypeScript 的一大目标就是让我们能够安全、轻松的使用现有的 JavaScript 库,它通过声明做到了这一点。TypeScript 提供了个动态的标准来衡量我们在声明中的投入,投入的越多,获得的类型安全和代码只能提示越多。需要注意的是,大多数的流行JavaScript库都有自己的声明文件。这里不再细说声明文件,后面会有一篇来专门介绍声明文件。

2. 支持未来JavaScript所有功能

虽然 TypeScript 是 JavaScript 的超集,但它始终紧跟ECMAScript标准,所以是支持ES6/7/8/9 等新语法标准的。并且,在语法层面上对一些语法进行了扩展。TypeScript 团队也正在积极的添加新功能的支持,这些功能会随着时间的推移而越来越多,越来越全面。

虽然 TypeScript 比较严谨,但是它并没有让 JavaScript 失去其灵活性。TypeScript 由于兼容 JavaScript 所以灵活度可以媲美 JavaScript,比如可以在任何地方将类型定义为 any(当然,并不推荐这样使用),毕竟 TypeScript 对类型的检查严格程度是可以通过 tsconfig.json 来配置的。

二、快速搭建TypeScript开发环境

1. 开发工具

在搭建TypeScript环境之前,先来看看适合TypeScript的IDE,这里主要介绍Visual Studio Code,笔者就一直使用这款编辑器。

VS Code可以说是微软的亲儿子了,其具有以下优势:

  • 在传统语法高亮、自动补全功能的基础上拓展了基于变量类型、函数定义,以及引入模块的智能补全;
  • 支持在编辑器上直接运行和调试应用;
  • 内置了 Git Comands,能大幅提升使用 Git 协同开发效率;
  • 有丰富的插件市场,可以根据需要选择适合的插件。

因为 VS Code 中内置了特定版本的 TypeScript 语言服务,所以它天然支持 TypeScript 语法解析和类型检测,且这个内置的服务与手动安装的 TypeScript 完全隔离。因此,VS Code 支持在内置和手动安装版本之间动态切换语言服务,从而实现对不同版本的 TypeScript 的支持。

如果当前应用目录中安装了与内置服务不同版本的 TypeScript,我们就可以点击 VS Code 底部工具栏的版本号信息,从而实现 “use VS Code's Version” 和 “use Workspace's Version” 两者之间的随意切换。

除此之外,VS Code 也基于 TypeScript 语言服务提供了准确的代码自动补全功能,并显示详细的类型定义信息,大大的提升了我们的开发效率。

2. 搭建开发环境

(1)代码初始化

1)全局安装TypeScript:

// npm
npm install -g typescript
// yarm
yarn global add typescript
// 查看版本
tsc -v

2)初始化配置文件:

tsc --init

执行之后,项目根目录会出现一个 tsconfig.json 文件,里面包含ts的配置项(可能因为版本不同而配置略有不同)。

{
  "compilerOptions": {
    "target""es5",                        // 指定 ECMAScript 目标版本: 'ES5'
    "module""commonjs",                   // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "moduleResolution""node",             // 选择模块解析策略
    "experimentalDecorators"true,         // 启用实验性的ES装饰器
    "allowSyntheticDefaultImports"true,   // 允许从没有设置默认导出的模块中默认导入。
    "sourceMap"true,                      // 把 ts 文件编译成 js 文件的时候,同时生成对应的 map 文件
    "strict"true,                         // 启用所有严格类型检查选项
    "noImplicitAny"true,                  // 在表达式和声明上有隐含的 any类型时报错
    "alwaysStrict"true,                   // 以严格模式检查模块,并在每个文件里加入 'use strict'
    "declaration"true,                    // 生成相应的.d.ts文件
    "removeComments"true,                 // 删除编译后的所有的注释
    "noImplicitReturns"true,              // 不是函数的所有返回路径都有返回值时报错
    "importHelpers"true,                  // 从 tslib 导入辅助工具函数
    "lib": ["es6""dom"],                  // 指定要包含在编译中的库文件
    "typeRoots": ["node_modules/@types"],
    "outDir""./dist",
    "rootDir""./src"
  },
  "include": [                              // 需要编译的ts文件 *表示文件匹配 **表示忽略文件的深度问题
    "./src/**/*.ts"
  ],  
  "exclude": [                // 不需要编译的ts文件
    "node_modules",
    "dist",
    "**/*.test.ts",
  ]
}

可以在package.json中加入script命令:

{
  "name""ts-demo",
  "version""1.0.0",
  "description""",
  "main""src/index.ts",
  "scripts": {
    "build""tsc",     // 执行编译
    "build:w""tsc -w" // 监听变化
  },
  "author""",
  "license""ISC",
  "devDependencies": {
    "TypeScript ""^4.1.2"
  }
}

3)编译ts代码:

tsc index.ts

(2)配置TSLint

TSLint 是一个通过tslint.json进行配置的插件,在编写TypeScript代码时,可以对代码风格进行检查和提示。如果对代码风格有要求,就需要用到TSLint了。其使用步骤如下:(1)在全局安装TSLint:

npm install tslint -g

(2)使用TSLint初始化配置文件:

tslint -i

执行之后,项目根目录下多了一个tslint.json文件,这就是TSLint的配置文件了,它会根据这个文件对代码进行检查,生成的tslint.json文件有下面几个字段:

{
  "defaultSeverity""error",
  "extends": [
    "tslint:recommended"
  ],
  "jsRules": {},
  "rules": {},
  "rulesDirectory": []
}

这些字段的含义如下;

  • defaultSeverity:提醒级别,如果为error则会报错,如果为warning则会警告,如果设为off则关闭,那TSLint就关闭了;
  • Ty
  • extends: 可指定继承指定的预设配置规则;
  • jsRules: 用来配置对.js.jsx文件的校验,配置规则的方法和下面的rules一样;
  • rules: TSLint检查代码的规则都是在这个里面进行配置,比如当我们不允许代码中使用eval方法时,就要在这里配置"no-eval": true
  • rulesDirectory: 可以指定规则配置文件,这里指定相对路径。

三、简单基础类型

在说TypeScript数据类型之前,先来看看在TypeScript中定义数据类型的基本语法。

在语法层面,缺省类型注解的 TypeScript 与 JavaScript 完全一致。因此,可以把 TypeScript 代码的编写看作是为 JavaScript 代码添加类型注解。

在 TypeScript 语法中,类型的标注主要通过类型后置语法来实现:“变量: 类型

let num = 996
let num: number = 996

上面代码中,第一行的语法是同时符合 JavaScript 和 TypeScript 语法的,这里隐式的定义了num是数字类型,我们就不能再给num赋值为其他类型。而第二行代码显式的声明了变量num是数字类型,同样,不能再给num赋值为其他类型,否则就会报错。

在 JavaScript 中,原始类型指的是非对象且没有方法的数据类型,包括:number、boolean、string、null、undefined、symbol、bigInt。

它们对应的 TypeScript 类型如下:

JavaScript原始基础类型TypeScript类型
numbernumber
booleanboolean
stringstring
nullnull
undefinedundefined
symbolsymbol
bigIntbigInt

需要注意numberNumber的区别:TypeScript中指定类型的时候要用 number ,这是TypeScript的类型关键字。而 Number 是 JavaScript 的原生构造函数,用它来创建数值类型的值,这两个是不一样的。包括stringboolean等都是TypeScript的类型关键字,而不是JavaScript语法。

1. number

TypeScript 和 JavaScript 一样,所有数字都是浮点数,所以只有一个 number 类型。

TypeScript 还支持 ES6 中新增的二进制和八进制字面量,所以 TypeScript 中共支持2、8、10和16这四种进制的数值:

let num: number;
num = 123;
num = "123";     // error 不能将类型"123"分配给类型"number"
num = 0b1111011// 二进制的123
num = 0o173;     // 八进制的123
num = 0x7b;      // 十六进制的123

2. string

字符串类型可以使用单引号和双引号来包裹内容,但是如果使用 Tslint 规则,会对引号进行检测,使用单引号还是双引号可以在 Tslint 规则中进行配置。除此之外,还可以使用 ES6  中的模板字符串来拼接变量和字符串会更为方便。

let str: string = "Hello World";
str = "Hello TypeScript";
const first = "Hello";
const last = "TypeScript";
str = `${first} ${last}`;
console.log(str) // 结果: Hello TypeScript

3. boolean

类型为布尔值类型的变量的值只能是true或者false。除此之外,赋值给布尔值的值也可以是一个计算之后结果为布尔值的表达式:

let bool: boolean = false;
bool = true;

let bool: boolean = !!0
console.log(bool) // false

4. null和undefined

在 JavaScript 中,undefined和 null 是两个基本数据类型。在 TypeScript 中,这两者都有各自的类型,即 undefined 和 null,也就是说它们既是实际的值,也是类型。这两种类型的实际用处不是很大。

let u: undefined = undefined;
let n: null = null;

注意,第一行代码可能会报一个tslint的错误:Unnecessary initialization to 'undefined',就是不能给一个变量赋值为undefined。但实际上给变量赋值为undefined是完全可以的,所以如果想让代码合理化,可以配置tslint,将"no-unnecessary-initializer"设置为false即可。

默认情况下,undefined 和 null 是所有类型的子类型,可以赋值给任意类型的值,也就是说可以把 undefined 赋值给 void 类型,也可以赋值给 number 类型。当在 tsconfig.json 的"compilerOptions"里设置为 "strictNullChecks": true 时,就必须严格对待了。这时 undefined 和 null 将只能赋值给它们自身或者 void 类型。这样也可以规避一些错误。

5. bigInt

BigInt是ES6中新引入的数据类型,它是一种内置对象,它提供了一种方法来表示大于 2- 1 的整数,BigInt可以表示任意大的整数。

使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了JavaScript构造函数 Number 能够表示的安全整数范围。

我们知道,在 JavaScript 中采用双精度浮点数,这导致精度有限,比如 Number.MAX_SAFE_INTEGER 给出了可以安全递增的最大可能整数,即2- 1,来看一个例子:

const max = Number.MAX_SAFE_INTEGER;
const max1 = max + 1
const max2 = max + 2
max1 === max2     // true

可以看到,最终返回了true,这就是超过精读范围造成的问题,而BigInt正是解决这类问题而生的:

const max = BigInt(Number.MAX_SAFE_INTEGER);
const max1 = max + 1n
const max2 = max + 2n
max1 === max2    // false

这里需要用 BigInt(number) 把 Number 转化为 BigInt,同时如果类型是 BigInt ,那么数字后面需要加 n

在TypeScript中,number 类型虽然和 BigInt 都表示数字,但是实际上两者类型是完全不同的:

declare let foo: number;
declare let bar: bigint;
foo = bar; // error: Type 'bigint' is not assignable to type 'number'.
bar = foo; // error: Type 'number' is not assignable to type 'bigint'.

6. symbol

symbol我们平时用的比较少,所以可能了解也不是很多,这里就详细来说说symbol。

(1)symbol 基本使用

symbol 是 ES6 新增的一种基本数据类型,它用来表示独一无二的值,可以通过 Symbol 构造函数生成。

const s = Symbol(); 
typeof s; // symbol

注意:Symbol 前面不能加 new关键字,直接调用即可创建一个独一无二的 symbol 类型的值。

可以在使用 Symbol 方法创建 symbol 类型值的时候传入一个参数,这个参数需要是一个字符串。如果传入的参数不是字符串,会先自动调用传入参数的 toString 方法转为字符串:

const s1 = Symbol("TypeScript"); 
const s2 = Symbol("Typescript"); 
console.log(s1 === s2); // false

上面代码的第三行可能会报一个错误:This condition will always return 'false' since the types 'unique symbol' and 'unique symbol' have no overlap. 这是因为编译器检测到这里的 s1 === s2 始终是false,所以编译器提醒这代码写的多余,建议进行优化。

上面使用Symbol创建了两个symbol对象,方法中都传入了相同的字符串,但是两个symbol值仍然是false,这就说明了 Symbol 方法会返回一个独一无二的值。Symbol 方法传入的这个字符串,就是方便我们区分 symbol 值的。可以调用 symbol 值的 toString 方法将它转为字符串:

const s1 = Symbol("Typescript"); 
console.log(s1.toString());  // 'Symbol(Typescript)'
console.log(Boolean(s));     // true 
console.log(!s);             // false

在TypeScript中使用symbol就是指定一个值的类型为symbol类型:

let a: symbol = Symbol()

TypeScript 中还有一个 unique symbol 类型,它是symbol的子类型,这种类型的值只能由Symbol()Symbol.for()创建,或者通过指定类型来指定变量是这种类型。这种类型的值只能用于常量的定义和用于属性名。需要注意,定义unique symbol类型的值,必须用 const 而不能用let来声明。下面来看在TypeScript中使用Symbol值作为属性名的例子:

const key1: unique symbol = Symbol()
let key2: symbol = Symbol()
const obj = {
    [key1]: 'value1',
    [key2]: 'value2'
}
console.log(obj[key1]) // value1
console.log(obj[key2]) // error 类型“symbol”不能作为索引类型使用。

(2)symbol 作为属性名

在ES6中,对象的属性是支持表达式的,可以使用于一个变量来作为属性名,这对于代码的简化有很多用处,表达式必须放在大括号内:

let prop = "name"
const obj = { 
  [prop]: "TypeScript" 
};
console.log(obj.name); // 'TypeScript'

symbol 也可以作为属性名,因为symbol的值是独一无二的,所以当它作为属性名时,不会与其他任何属性名重复。当需要访问这个属性时,只能使用这个symbol值来访问(必须使用方括号形式来访问):

let name = Symbol(); 
let obj = { 
  [name]: "TypeScript" 
};
console.log(obj); // { Symbol(): 'TypeScript' }

console.log(obj[name]); // 'TypeScript' 
console.log(obj.name);  // undefined

在使用obj.name访问时,实际上是字符串name,这和访问普通字符串类型的属性名是一样的,要想访问属性名为symbol类型的属性时,必须使用方括号。方括号中的name才是我们定义的symbol类型的变量name。

(3)symbol 属性名遍历

使用 Symbol 类型值作为属性名,这个属性是不会被 for…in遍历到的,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 、 JSON.stringify() 等方法获取到:

const name = Symbol("name"); 
const obj = { 
  [name]: "TypeScript"
  age: 18 
};
for (const key in obj) { 
  console.log(key); 
}  
// => 'age' 
console.log(Object.keys(obj));  // ['age'] 
console.log(Object.getOwnPropertyNames(obj));  // ['age'] 
console.log(JSON.stringify(obj)); // '{ "age": 18 }

虽然这些方法都不能访问到Symbol类型的属性名,但是Symbol类型的属性并不是私有属性,可以使用 Object.getOwnPropertySymbols 方法获取对象的所有symbol类型的属性名:

const name = Symbol("name"); 
const obj = { 
  [name]: "TypeScript"
  age: 18 
};
const SymbolPropNames = Object.getOwnPropertySymbols(obj); 
console.log(SymbolPropNames); // [ Symbol(name) ] 
console.log(obj[SymbolPropNames[0]]); // 'TypeScript'

除了这个方法,还可以使用ES6提供的 Reflect 对象的静态方法 Reflect.ownKeys ,它可以返回所有类型的属性名,Symbol 类型的也会返回:

const name = Symbol("name"); 
const obj = { 
  [name]: "TypeScript"
  age: 18 
};
console.log(Reflect.ownKeys(obj)); // [ 'age', Symbol(name) ]

(4)symbol 静态方法

Symbol 包含两个静态方法, for 和 keyFor 。1)Symbol.for()

用Symbol创建的symbol类型的值都是独一无二的。使用 Symbol.for 方法传入字符串,会先检查有没有使用该字符串调用 Symbol.for 方法创建的 symbol 值。如果有,返回该值;如果没有,则使用该字符串新创建一个。使用该方法创建 symbol 值后会在全局范围进行注册。

const iframe = document.createElement("iframe"); 
iframe.src = String(window.location); 
document.body.appendChild(iframe); 

iframe.contentWindow.Symbol.for("TypeScript") === Symbol.for("TypeScript"); // true // 注意:如果你在JavaScript环境中这段代码是没有问题的,但是如果在TypeScript开发环境中,可能会报错:类型“Window”上不存在属性“Symbol”。 // 因为这里编译器推断出iframe.contentWindow是Window类型,但是TypeScript的声明文件中,对Window的定义缺少Symbol这个字段,所以会报错,

上面代码中,创建了一个iframe节点并把它放在body中,通过这个 iframe 对象的 contentWindow 拿到这个 iframe 的 window 对象,在 iframe.contentWindow上添加一个值就相当于在当前页面定义一个全局变量一样。可以看到,在 iframe 中定义的键为 TypeScript 的 symbol 值在和在当前页面定义的键为'TypeScript'的symbol 值相等,说明它们是同一个值。

2)Symbol.keyFor()该方法传入一个 symbol 值,返回该值在全局注册的键名:

const sym = Symbol.for("TypeScript"); 
console.log(Symbol.keyFor(sym)); // 'TypeScript'

四、复杂基础类型

看完简单的数据类型,下面就来看看比较复杂的数据类型,包括JavaScript中的数组和对象,以及TypeScript中新增的元组、枚举、Any、void、never、unknown。

1. array

在 TypeScript 中有两种定义数组的方式:

  • 直接定义: 通过 number[] 的形式来指定这个类型元素均为number类型的数组类型,推荐使用这种写法。
  • 数组泛型: 通过 Array 的形式来定义,使用这种形式定义时,tslint 可能会警告让我们使用第一种形式定义,可以通过在tslint.json 的 rules 中加入 "array-type": [false] 就可以关闭 tslint 对这条的检测。
let list1: number[] = [123];
let list2: Array<number> = [123];

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

注意,这两种写法中的 number 指定的是数组元素的类型,也可以在这里将数组的元素指定为其他任意类型。如果要指定一个数组里的元素既可以是数值也可以是字符串,那么可以使用这种方式:number|string[]

2. object

在JavaScript中,object是引用类型,它存储的是值的引用。在TypeScript中,当想让一个变量或者函数的参数的类型是一个对象的形式时,可以使用这个类型:

let obj: object
obj = { name: 'TypeScript' }
obj = 123             // error 不能将类型“123”分配给类型“object”
console.log(obj.name) // error 类型“object”上不存在属性“name”

可以看到,当给一个对象类型的变量赋值一个对象时,就会报错。对象类型更适合以下场景:

function getKeys (obj: object) {
  return Object.keys(obj) // 会以列表的形式返回obj中的值
}
getKeys({ a: 'a' }) // ['a']
getKeys(123)        // error 类型“123”的参数不能赋给类型“object”的参数

3. 元组

在 JavaScript 中并没有元组的概念,作为一门动态类型语言,它的优势是支持多类型元素数组。但是出于较好的扩展性、可读性和稳定性考虑,我们通常会把不同类型的值通过键值对的形式塞到一个对象中,再返回这个对象,而不是使用没有任何限制的数组。TypeScript 的元组类型正好弥补了这个不足,使得定义包含固定个数元素、每个元素类型未必相同的数组成为可能。

元组可以看做是数组的扩展,它表示已知元素数量和类型的数组,它特别适合用来实现多值返回。确切的说,就是已知数组中每一个位置上的元素的类型,可以通过元组的索引为元素赋值::

let arr: [stringnumberboolean];
arr = ["a"2false]; // success
arr = [2"a"false]; // error 不能将类型“number”分配给类型“string”。 不能将类型“string”分配给类型“number”。
arr = ["a"2];        // error Property '2' is missing in type '[string, number]' but required in type '[string, number, boolean]'
arr[1] = 996

可以看到,定义的arr元组中,元素个数和元素类型都是确定的,当为arr赋值时,各个位置上的元素类型都要对应,元素个数也要一致。

当访问元组元素时,TypeScript也会对元素做类型检查,如果元素是一个字符串,那么它只能使用字符串方法,如果使用别的类型的方法,就会报错。

在TypeScript 新的版本中,TypeScript会对元组做越界判断。超出规定个数的元素称作越界元素,元素赋值必须类型和个数都对应,不能超出定义的元素个数。

在新的版本中,[string, number]元组类型的声明效果上可以看做等同于下面的声明:

interface Tuple extends Array<number | string> { 
   0string
   1number;
   length: 2
}

这里定义了接口 Tuple ,它继承数组类型,并且数组元素的类型是 number 和 string 构成的联合类型,这样接口 Tuple 就拥有了数组类型所有的特性。并且指定索引为0的值为 string 类型,索引为1的值为 number 类型,同时指定 length 属性的类型字面量为 2,这样在指定一个类型为这个接口 Tuple 时,这个值必须是数组,而且如果元素个数超过2个时,它的length就不是2是大于2的数了,就不满足这个接口定义了,所以就会报错;当然,如果元素个数不够2个也会报错,因为索引为0或1的值缺失。

4. 枚举

TypeScript 在 ES 原有类型基础上加入枚举类型,使得在 TypeScript 中也可以给一组数值赋予名字,这样对开发者比较友好。枚举类型使用enum来定义:

enum Roles {
  SUPER_ADMIN,
  ADMIN,
  USER
}

上面定义的枚举类型的Roles,它有三个值,TypeScript会为它们每个值分配编号,默认从0开始,在使用时,就可以使用名字而不需要记数字和名称的对应关系了:

enum Roles {
  SUPER_ADMIN = 0,
  ADMIN = 1,
  USER = 2 
}

const superAdmin = Roles.SUPER_ADMIN;
console.log(superAdmin); // 0
console.log(Roles[1])    // ADMIN

除此之外,还可以修改这个数值,让SUPER_ADMIN = 1,这样后面的值就分别是2和3。当然还可以给每个值赋予不同的、不按顺序排列的值:

enum Roles {
   SUPER_ADMIN = 1,
   ADMIN = 3,
   USER = 7 
}

5. any

在编写代码时,有时并不清楚一个值是什么类型,这时就需要用到any类型,它是一个任意类型,定义为any类型的变量就会绕过TypeScript的静态类型检测。对于声明为any类型的值,可以对其进行任何操作,包括获取事实上并不存在的属性、方法,并且 TypeScript 无法检测其属性是否存在、类型是否正确。

我们可以将一个值定义为any类型,也可以在定义数组类型时使用any来指定数组中的元素类型为任意类型:

let value: any;
value = 123;
value = "abc";
value = false;

const array: any[] = [1"a"true];

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

let obj: any = {};
let z = obj.x.y.z; // z的类型是any,不会报错
z();               // success

需要注意:不要滥用any类型,如果代码中充满了any,那TypeScript和JavaScript就毫无区别了,所以除非有充足的理由,否则应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。

6. void

void 和 any 相反,any 是表示任意类型,而 void 是表示没有类型,就是什么类型都不是。这在定义函数,并且函数没有返回值时会用到

const consoleText = (text: string): void => {
  console.log(text); 
};

需要注意:void 类型的变量只能赋值为 undefined 和 null ,其他类型不能赋值给 void 类型的变量。

7. never

never 类型指永远不存在值的类型,它是那些总会抛出异常根本不会有返回值的函数表达式的返回值类型,当变量被永不为真的类型保护所约束时,该变量也是 never 类型。

下面的函数,总是会抛出异常,所以它的返回值类型是never,用来表明它的返回值是不存在的:

const errorFunc = (message: string): never => {
   throw new Error(message); 
};

never 类型是任何类型的子类型,所以它可以赋值给任何类型;而没有类型是 never 的子类型,所以除了它自身以外,其他类型(包括 any 类型)都不能为 never 类型赋值。

let neverVariable = (() => {
   while (true) {} 
})();
neverVariable = 123; // error 不能将类型"number"分配给类型"never"

上面代码定义了一个立即执行函数,函数体是一个死循环,这个函数调用后的返回值类型为 never,所以赋值之后 neverVariable 的类型是 never 类型,当给neverVariable 赋值 123 时,就会报错,因为除它自身外任何类型都不能赋值给 never 类型。

基于 never 的特性,我们可以把 never 作为接口类型下的属性类型,用来禁止操作接口下特定的属性:

const props: {
  id: number,
  name?: never
} = {
  id: 1
}
props.name = null;  // error
props.name = 'str'// error
props.name = 1;     // error

可以看到,无论给 props.name 赋什么类型的值,它都会提示类型错误,这就相当于将 name 属性设置为了只读 。

8. unknown

unknown 是TypeScript在3.0版本新增的类型,主要用来描述类型并不确定的变量。它看起来和any很像,但是还是有区别的,unknown相对于any更安全。

对于any,来看一个例子:

let value: any
console.log(value.name)
console.log(value.toFixed())
console.log(value.length)

上面这些语句都不会报错,因为value是any类型,所以后面三个操作都有合法的情况,当value是一个对象时,访问name属性是没问题的;当value是数值类型的时候,调用它的toFixed方法没问题;当value是字符串或数组时获取它的length属性是没问题的。

当指定值为unknown类型的时候,如果没有缩小类型范围的话,是不能对它进行任何操作的。总之,unknown类型的值不能随便操作。那什么是类型范围缩小呢?下面来看一个例子:

function getValue(value: unknown): string {
  if (value instanceof Date) { 
    return value.toISOString();
  }
  return String(value);
}

这里由于把value的类型缩小为Date实例的范围内,所以进行了value.toISOString(),也就是使用ISO标准将 Date 对象转换为字符串。

使用以下方式也可以缩小类型范围:

let result: unknown;
if (typeof result === 'number') {
  result.toFixed();
}

关于 unknown 类型,在使用时需要注意以下几点:

  • 任何类型的值都可以赋值给 unknown 类型:
let value1: unknown;
value1 = "a";
value1 = 123;
  • unknown 不可以赋值给其它类型,只能赋值给 unknown 和 any 类型:
let value2: unknown;
let value3: string = value2; // error 不能将类型“unknown”分配给类型“string”
value1 = value2;
  • unknown 类型的值不能进行任何操作:
let value4: unknown;
value4 += 1// error 对象的类型为 "unknown"
  • 只能对 unknown 进行等或不等操作,不能进行其它操作:
value1 === value2;
value1 !== value2;
value1 += value2;  // error
  • unknown 类型的值不能访问其属性、作为函数调用和作为类创建实例:
let value5: unknown;
value5.age;   // error
value5();     // error
new value5(); // error

在实际使用中,如果有类型无法确定的情况,要尽量避免使用 any,因为 any 会丢失类型信息,一旦一个类型被指定为 any,那么在它上面进行任何操作都是合法的,所以会有意想不到的情况发生。因此如果遇到无法确定类型的情况,要先考虑使用 unknown。

这篇文章到这里就结束了,如果觉得有用就点个赞吧~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存