Skip to content

常见类型

TypeScript 提供了多种内置的类型,可以用来定义变量、函数参数、返回值等。了解这些常见类型有助于编写更安全和可维护的代码。以下是 TypeScript 中的一些基本类型:

基本数据类型(值类型)

基本数据类型存储的是值本身的副本,存储在栈内存中。当复制这类数据时,会创建一个全新的拷贝,修改其中一个副本不会影响到另一个。基本数据类型包括:Number、String、Boolean、Null、Undefined、Void、Never、Symbol、BigInt。

数字类型(number)

ts
let count: number = 10;
// ES5:var count = 10;

字符串类型(string)

ts
let name: string = "Semliker";
// ES5:var name = 'Semlinker';

布尔类型(boolean)

ts
let isDone: boolean = false;
// ES5:var isDone = false;

空值类型(void)

void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:

ts
function warnUser(): void {
  console.log("This is my warning message");
}

声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null:

ts
let unusable: void = undefined;

null 和 undefined 类型

TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null。和 void 相似,它们的本身的类型用处不是很大:

ts
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

然而,当你指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。

never 类型

never 类型表示的是那些永不存在的值的类型。 例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never 类型,当它们被永不为真的类型保护所约束时。

symbol 类型

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

bigInt 类型

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

引用数据类型

引用数据类型存储的是值的引用(内存地址),而不是值本身,它们存储在堆内存中,而变量存储的是指向堆内存中对象的地址。复制引用类型的数据时,实际上是复制了指向同一个对象的引用,因此修改其中一个引用会影响所有指向该对象的变量。引用数据类型包括:Object、Array、Function、Class、Interface、Tuple、Enum、Any、Unknown。

基本数据类型和引用数据类型的主要区别在于存储和复制时的行为。基本数据类型直接存储值,复制时会产生独立的拷贝;而引用数据类型存储的是值的引用,复制时共享同一份数据。理解这些差异对于编写高效、安全的 TypeScript 代码至关重要。

对象类型(object)

object 表示非原始类型,也就是除 number,string,boolean,symbol,null 或 undefined 之外的类型。

使用 object 类型,就可以更好的表示像 Object.create 这样的 API。例如:

ts
declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

数组类型(array)

ts
let list: number[] = [1, 2, 3];
let list: (string | number)[] = [1, "2", 3];
// ES5:var list = [1,2,3];

let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
let list: Array<number | string> = [1, "2", 3];
let list: Array<number> | Array<string> = ["1", "2", "3"];
// ES5:var list = [1,2,3];

函数类型(function)

Function,函数也是对象,可以被赋值给变量、作为参数传递或作为其他对象的属性。

类类型(class)

Class,面向对象编程中的类,是创建对象的蓝图。

接口类型(interface)

Interface,接口定义了对象的形状,虽然接口本身不是值,但实现接口的对象是引用类型。

元组类型(tuple)

TypeScript 中的元组类型(Tuple Type)是一种特殊的数据结构,它允许你创建数组,但这个数组的长度是固定的,并且数组中的每个元素都有其预定义的具体类型。简单来说,元组就是一种拥有固定数量和不同类型的元素的数组。

ts
let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error

元组类型的应用场景包括但不限于:

  • 函数返回多个不同类型的结果:当一个函数需要返回多种类型的数据时,可以使用元组类型来封装这些返回值。
  • 处理固定结构的数据:比如在处理数据库查询结果、坐标点 (x, y)颜色 (red, green, blue) 等具有固定数量和类型的元素时,元组类型非常适合。
  • 函数参数的类型注解:元组也可以用来精确地定义函数接收的参数类型和顺序,特别是当参数需要按特定类型组合时。

在 TypeScript (TS) 中,元组(Tuple)数组(Array)都是用来存储多个值的数据结构,但它们之间存在几个关键区别:

  • 元组(Tuple)类型多样性,长度固定,结构化数据。
  • 数组(Array)类型一致性,长度可变, 用途广泛。

因此,元组(Tuple)更适合用于存储已知数量和类型的数据,而数组(Array)更适合用于存储数量不定的数据,并且可以存储不同类型的元素。

枚举类型(enum)

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

  1. 数字枚举
ts
// 默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH; // dir=0

// 手动赋值
enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH; // dir=3
  1. 字符串枚举 在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

    ts
    // 默认情况下,NORTH 的初始值为 "NORTH",其余的成员会从 1 开始自动增长。
    enum Direction {
      NORTH = "NORTH",
      SOUTH = "SOUTH",
      EAST = "EAST",
      WEST = "WEST",
    }
    
    let dir: Direction = Direction.NORTH; // dir="NORTH"
  2. 异构枚举

异构枚举的成员值是数字和字符串的混合。

ts
enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

// A, B, C, D, E, F 的初始值为 0, 1, "C", "D", 8, 9

任意类型(any)

在 TypeScript 中,任何类型都可以被归为 any 类型(兜底类型)。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

ts
let notSure: any = 666;
notSure = "Semlinker";
notSure = false;

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。

ts
let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

Unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。

ts
let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

对 value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

ts
let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

ts
let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

将 value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

运算符

联合类型(union)

联合类型表示取值可以为多种类型中的一种。联合类型强调的是“或”的关系,表示一个值可以是几个类型中的任意一种。

ts
let myFavoriteNumber: string | number;
myFavoriteNumber = "seven";
myFavoriteNumber = 7;

在使用联合类型的变量时,只有那些所有类型中共有的成员才能被直接访问,如果需要访问特定类型的成员,需要通过来确定类型。

交叉类型(intersection)

交叉类型表示取值是多个类型中的交叉部分。交叉类型强调的是“且”的关系,表示一个值需要同时满足多个类型的要求,具备这些类型的全部特性。

ts
type PartialPointX = { x: number };
type PartialPointY = { y: number };

type PartialPoint = PartialPointX & PartialPointY;

// 等同于
type PartialPoint = { x: number } & { y: number };

交叉类型允许你组合多个类型的成员到一个新的类型中,这样就可以在一个对象上同时拥有所有组合类型的属性和方法。

类型断言(as)

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在起作用。

类型断言主要用于告知编译器你确信一个值属于某种类型,以便在静态类型检查期间能够访问该类型的特性。它不改变值的实际类型,仅在编译时起作用。

类型断言有两种形式。 其一是“尖括号”语法:

ts
let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

另一个为 as 语法:

ts
let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript 中使用 JSX 时,只有 as 语法断言是被允许的。

类型守卫(is)

类型守卫是通过某种方式识别出变量是否属于某个类型。 类型守卫的原理是,TypeScript 会分析代码中类型守卫的逻辑,然后使用类型分析的结果缩小变量的类型范围。

ts
function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

function isBoolean(x: any): x is boolean {
  return typeof x === "boolean";
}

TypeScript 提供了几种主要的类型守卫方式:

  • typeof:用于检查变量是否为特定的基本类型(如 string、number、boolean、undefined、object、function、symbol 或 bigint)。
  • instanceof:用于检查一个对象是否为某个构造函数的实例。
  • in:用于检查一个属性是否存在于某个对象或其原型链中。
  • Array.isArray:用于检查一个值是否为数组。
  • 自定义类型保护函数:通过用户自定义的函数,利用类型谓词(返回值为布尔类型的函数,并且形参带有类型断言)来实现类型守卫。
ts
interface Animal {
  name: string;
  meow(): void;
}

interface Cat extends Animal {
  meow(): void;
}

function isCat(animal: Animal): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

function petAnimal(animal: Animal) {
  if (isCat(animal)) {
    // 在这里 animal 被确定为 Cat 类型
    // 若isCat(): boolean,则 animal 类型为 Animal,此处会报错,类型“Animal”上不存在属性“meow”
    /*
    这个函数的作用是检查一个 Animal 是否具有 Cat 的特征(在这里通过检查是否有 meow 方法)。如果 animal 能够安全地被断言为 Cat 类型(即拥有 meow 方法),函数返回 true,此时TypeScript编译器就会“记住”这个信息,并在接下来的代码中将 animal 当作 Cat 类型处理。
    */
    animal.meow();
  } else {
    console.log("Petting the animal");
  }
}

TypeScript 语法

类型别名(type)

当我们想要多次在其他地方使用时就编写多次。这时我们可以给对象类型起一个别名。

ts
// 类型别名: type
type MyNumber = number;
const age: MyNumber = 18;

// 给ID的类型起一个别名
type IDType = number | string;

function printID(id: IDType) {
  console.log(id);
}

// 打印坐标
type PointType = { x: number; y: number; z?: number };
function printCoordinate(point: PointType) {
  console.log(point.x, point.y, point.z);
}

类型声明(declare)

在 TypeScript 中,declare 关键字主要用于以下几种场景,它帮助在编译时声明现有的 JavaScript 代码中的类型信息,而无需实际提供实现细节。这对于类型定义文件(.d.ts)非常关键,这些文件通常用于描述已有的 JavaScript 库或模块的类型信息。

类型推断(infer)

在 TypeScript 中,infer 关键字用于在条件类型(Conditional Types)中从联合类型或其它类型结构中推断出具体的类型。infer 主要是在类型系统内部使用,特别是在定义泛型类型别名时,帮助从复杂的类型表达式中提取或推断出类型。

ts
type MyExtract<T, U> = T extends U ? T : never;
type InferProperty<T, Key extends keyof T> = T[Key];
type PickCommonProperty<T extends object[]> = {
  [K in keyof T]: InferProperty<T[K], infer P> extends infer R ? R : never;
}[number];

// 使用示例
type Result = PickCommonProperty<
  [{ a: number }, { a: number; b: string }, { a: number; c: boolean }]
>;
// 结果是: number

类型兼容性(compatible)

在 TypeScript 中,规则决定了一个类型是否可以被赋值给另一个类型。这是静态类型检查的核心部分,帮助确保类型安全。TypeScript 的兼容性原则广泛但遵循一些基本原则,旨在保持灵活性的同时确保类型的一致性和安全性。

类型保护(protect)

是 TypeScript 中用于在运行时缩小类型范围,使得编译器能够更准确地推断变量的类型,从而允许更安全地访问类型相关的属性或方法。类型保护机制帮助开发者在类型系统中做出更精确的类型判断,避免在条件语句中出现潜在的类型错误。

类型别名(alias)

在 TypeScript 中是一种给类型起一个新名字的方式,使得你可以使用更易读或更具有描述性的名称来引用复杂类型,提高代码的可读性和可维护性。类型别名不创建新的类型,它仅仅为已存在的类型提供一个新的名字。

参考资料

线上 TypeScript Playground

Released under the MIT License.