跳到主要内容

类型

备注

在 JavaScript 中,原始类型指的是非对象且没有方法的数据类型,它包括 string、number、bigint、boolean、undefined 和 symbol这六种 (null 是一个伪原始类型,它在 JavaScript 中实际上是一个对象,且所有的结构化类型都是通过 null 原型链派生而来)

在 JavaScript 语言中,原始类型值是最底层的实现,对应到 TypeScript 中同样也是最底层的类型。

基础类型

所有 JavaScript 支持的定义字符串的方法,都可以直接在 TypeScript 中使用
let firstname: string = 'Captain'; // 字符串字面量
let familyname: string = String('S'); // 显式类型转换
let fullname: string = `my name is ${firstname}.${familyname}`; // 模板字符串

元组

元组可以限制数组元素的个数和类型,它特别适合用来实现多值返回。

demo
import { useState } from 'react';

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

// useState 的返回值类型是一个元组类型
(state: State) => [State, SetState]

元组相较对象而言,不仅为我们实现解构赋值提供了极大便利,还减少了不少代码量,这可能也是 React 官方如此设计核心 Hooks 的重要原因之一。

但事实上,许多第三方的 Hooks 往往会出于扩展性、稳定性等考虑,尤其是需要返回的值的个数超过 2 个时,会更偏向于使用对象作为返回值。

提示

这里需要注意:数组类型的值只有显示添加了元组类型注解后(或者使用 as const,声明为只读元组),TypeScript 才会把它当作元组,否则推荐出来的类型就是普通的数组类型。

📢注意点
  • 虽然 numberbigint 都表示数字,但是这两个类型不兼容。

  • 非严格模式下, string、 number、boolean 允许值为空(null)。严格模式下则不允许。

  • TypeScript 的数组和元组转译为 JavaScript 后都是数组。

  • 不要将 TypeScript 中 Number、String、Boolean、Symbol 等类型和小写格式对应的 number、string、boolean、symbol 进行等价。实际上,我们压根使用不到 Number、String、Boolean、Symbol 类型,因为它们并没有什么特殊的用途。这就像我们不必使用 Number、String、Boolean 等构造函数 new 一个相应的实例一样。

    let num: number;
    let Num: Number;
    Num = num; // ok
    num = Num; // ts(2322)

    📢 不要使用对象类型来注解值的类型,因为这没有任何意义!

特殊类型

备注

这是并不是 TypeScript 官方的定义

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 是地狱)

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

函数类型

备注

在 JavaScript 中,函数是构建应用的一块基石,我们可以使用函数抽离可复用的逻辑、抽象模型、封装过程。在 TypeScript 中,虽然有类、命名空间、模块,但是函数同样是最基本、最重要的元素之一。

// 在 TypeScript 里,可以通过 function 字面量和箭头函数的形式定义函数
function add() {}
const add = () => {};

// 可以显式指定函数参数和返回值的类型
const add = (a: number, b: number): number => {
return a + b;
};

返回值类型

在 JavaScript 中,一个函数可以没有显式 return,此时函数的返回值应该是 undefined

function fn() {
// TODO
}
console.log(fn()); // => undefined
提示

在 TypeScript 中,如果显式声明函数的返回值类型为 undfined,将会得到如下所示的错误提醒。

function fn(): undefined {
// ts(2355) A function whose declared type is neither 'void' nor 'any' must return a value
// TODO
}

正确的做法是使用 void 类型来表示函数没有返回值的类型

function fn1(): void {}
fn1().doSomething(); // ts(2339) Property 'doSomething' does not exist on type 'void'.

我们可以使用类似定义箭头函数的语法来表示函数类型的参数和返回值类型,此时=> 类型仅仅用来定义一个函数类型而不用实现这个函数。

需要注意的是,这里的=>与 ES6 中箭头函数的=>有所不同。TypeScript 函数类型中的=>用来表示函数的定义,其左侧是函数的参数类型,右侧是函数的返回值类型;而 ES6 中的=>是函数的实现。

如下示例中,我们定义了一个函数类型(这里我们使用了类型别名 type),并且使用箭头函数实现了这个类型。

type Adder = (a: number, b: number) => number; // TypeScript 函数类型定义
const add: Adder = (a, b) => a + b; // ES6 箭头函数

注意:右侧的箭头函数并没有显式声明类型注解,不过可以根据上下文类型进行推断。

在对象(即接口类型)中,除了使用这种声明语法,我们还可以使用类似对象属性的简写语法来声明函数类型的属性,如下代码所示:

interface Entity {
add: (a: number, b: number) => number;
del(a: number, b: number): number;
}
const entity: Entity = {
add: (a, b) => a + b,
del(a, b) {
return a - b;
},
};

在某种意义上来说,这两种形式都是等价的。但是很多时候,我们不必或者不能显式地指明返回值的类型。

可缺省和可推断的返回值类型

函数返回值的类型可以在 TypeScript 中被推断出来,即可缺省。

函数内是一个相对独立的上下文环境,我们可以根据入参对值加工计算,并返回新的值。从类型层面看,我们也可以通过类型推断加工计算入参的类型,并返回新的类型,示例如下:

function computeTypes(one: string, two: number) {
const nums = [two];
const strs = [one];
return {
nums,
strs,
}; // 返回 { nums: number[]; strs: string[] } 的类型
}

⚠️ 这是一个很重要也很有意思的特性,函数返回值的类型推断结合泛型可以实现特别复杂的类型计算(本质是复杂的类型推断,这里称之为计算是为了表明其复杂性),比如 Redux Model 中 State、Reducer、Effect 类型的关联。

一般情况下,TypeScript 中的函数返回值类型是可以缺省和推断出来的,但是有些特例需要我们显式声明返回值类型,比如 Generator 函数的返回值。

Generator 函数的返回值

ES6 中新增的 Generator 函数在 TypeScript 中也有对应的类型定义。

Generator 函数返回的是一个 Iterator 迭代器对象,我们可以使用 Generator 的同名接口泛型或者 Iterator 的同名接口泛型表示返回值的类型(Generator 类型继承了 Iterator 类型),示例如下:

type AnyType = boolean;
type AnyReturnType = string;
type AnyNextType = number;
function* gen(): Generator<AnyType, AnyReturnType, AnyNextType> {
const nextValue = yield true; // nextValue 类型是 number,yield 后必须是 boolean 类型
return `${nextValue}`; // 必须返回 string 类型
}
提示

注意:TypeScript 3.6 之前的版本不支持指定 next、return 的类型,所以在某些有点历史的代码中,我们可能会看到 Generator 和 Iterator 类型不一样的表述。

参数类型

可选参数和默认参数

在实际工作中,我们可能经常碰到函数参数可传可不传的情况,当然 TypeScript 也支持这种函数类型表达,如下代码所示:

function log(x?: string) {
return x;
}
log(); // => undefined
log("hello world"); // => hello world

在上述代码中,我们在类型标注的:前添加?表示 log 函数的参数 x 就是可缺省的。

也就是说参数 x 的类型可能是 undefined(第 5 行调用 log 时不传入实参)类型或者是 string 类型(第 6 行调用 log 传入 'hello world' 实参),那是不是意味着可缺省和类型是 undefined 等价呢?我们来看看以下的示例:

function log(x?: string) {
console.log(x);
}
function log1(x: string | undefined) {
console.log(x);
}
log();
log(undefined);
log1(); // ts(2554) Expected 1 arguments, but got 0
log1(undefined);

答案显而易见:这里的 ?: 表示参数可以缺省、可以不传,也就是说调用函数时,我们可以不显式传入参数。但是,如果我们声明了参数类型为 xxx | undefined(这里使用了联合类型 |),就表示函数参数是不可缺省且类型必须是 xxx 或者 undfined。

因此,在上述代码中,log1 函数如果不显示传入函数的参数,TypeScript 就会报一个 ts(2554) 的错误,即函数需要 1 个参数,但是我们只传入了 0 个参数。

在 ES6 中支持函数默认参数的功能,而 TypeScript 会根据函数的默认参数的类型来推断函数参数的类型,示例如下:

function log(x = "hello") {
console.log(x);
}
log(); // => 'hello'
log("hi"); // => 'hi'
log(1); // ts(2345) Argument of type '1' is not assignable to parameter of type 'string | undefined'

在上述示例中,根据函数的默认参数 'hello' ,TypeScript 推断出了 x 的类型为 string | undefined。

当然,对于默认参数,TypeScript 也可以显式声明参数的类型(一般默认参数的类型是参数类型的子集时,我们才需要这么做)。不过,此时的默认参数只起到参数默认值的作用,如下代码所示:

function log1(x: string = "hello") {
console.log(x);
}
// ts(2322) Type 'string' is not assignable to type 'number'
function log2(x: number = "hello") {
console.log(x);
}
log2();
log2(1);
log2("1"); // ts(2345) Argument of type '"1"' is not assignable to parameter of type 'number | undefined'

上例函数 log2 中,我们显式声明了函数参数 x 的类型为 number,表示函数参数 x 的类型可以不传或者是 number 类型。因此,如果我们将默认值设置为字符串类型,编译器就会抛出一个 ts(2322) 的错误。

同理,如果我们将函数的参数传入了字符串类型,编译器也会抛出一个 ts(2345) 的错误。

这里请注意:函数的默认参数类型必须是参数类型的子类型,下面我们看一下如下具体示例:

function log3(x: number | string = "hello") {
console.log(x);
}

在上述代码中,函数 log3 的函数参数 x 的类型为可选的联合类型 number | string,但是因为默认参数字符串类型是联合类型 number | string 的子类型,所以 TypeScript 也会检查通过。

剩余参数

在 ES6 中,JavaScript 支持函数参数的剩余参数,它可以把多个参数收集到一个变量中。同样,在 TypeScript 中也支持这样的参数类型定义,如下代码所示:

function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2); // => 3
sum(1, 2, 3); // => 6
sum(1, "2"); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number'

在上述代码中,sum 是一个求和的函数,...nums 将函数的所有参数收集到了变量 nums 中,而 nums 的类型应该是 number[],表示所有被求和的参数是数字类型。因此,sum(1, '2') 抛出了一个 ts(2345) 的错误,因为参数 '2' 并不是 number 类型。

如果我们将函数参数 nums 聚合的类型定义为 (number | string)[],如下代码所示:

function sum(...nums: (number | string)[]): number {
return nums.reduce<number>((a, b) => a + Number(b), 0);
}
sum(1, "2", 3); // 6

那么,函数的每一个参数的类型就是联合类型 number | string,因此 sum(1, '2', 3) 的类型检查也就通过了。