基础类型(始于 JS)


# 基础类型(始于 JS)

TypeScript 可以通过为 JavaScript 代码添加类型与类型检查来确保健壮性。因此「类型」它是最核心的部分。

在 TypeScript 语法中,类型的标注主要通过类型后置语法来实现,即用 : 作为分割变量和类型的分隔符。而没有写类型标注的 TypeScript 与 JavaScript 完全一致,因此可以把 TypeScript 代码的编写看作是为 JavaScript 代码添加类型标注。

# 原始类型的类型标注

JavaScript 的内置原始类型 (opens new window)中,除了常见的 number / string / boolean / null / undefined,ES6 和 ES11 又分别引入了 2 个新的原始类型:symbol 与 bigint 。在 TypeScript 中它们都有对应的类型标注:

const name: string = 'apple';
const age: number = 24;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 9007199254740991n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique');
1
2
3
4
5
6
7
8
9

其中,除了 nullundefined 比较特殊之外,余下的类型基本上可以完全对应到 JavaScript 中的数据类型概念。

# null 与 undefined

在 JavaScript 中,nullundefined 分别表示「有值,但是个空值」和「没有值」。

而在 TypeScript 中,nullundefined 类型都是有具体意义的类型。也就是说,它们作为类型时,表示的是一个有意义的具体类型值。这两者在没有开启 strictNullChecks 检查的情况下,会被视作其他类型的子类型,比如 string 类型会被认为包含了 nullundefined 类型:

// null 类型只能赋值为 null
const tmp1: null = null;
// undefined 类型只能赋值为 undefined
const tmp2: undefined = undefined;

// 仅在关闭 strictNullChecks 时成立,下面代码成立
const tmp3: string = null;
const tmp4: string = undefined;
1
2
3
4
5
6
7
8

# void

void 也是一个特殊的类型,它和 JavaScript 中的 void 同样不是一回事。

在 JavaScript 中,形如 <a href="javascript:void(0)">清除缓存</a> 的代码,void 操作符会执行后面跟着的表达式并返回一个 undefined

而在 TypeScript 中,void 用于描述一个内部没有 return 语句,或者没有显式 return 一个值的函数的返回值,如:

function func1() {}
function func2() {
  return;
}
function func3() {
  return undefined;
}
1
2
3
4
5
6
7

在这里,func1 与 func2 的返回值类型都会被隐式推导为 void,只有显式返回了 undefined 值的 func3 其返回值类型才被推导为了 undefined。但在实际的代码执行中,func1 与 func2 的返回值均是 undefined

这里可以认为 void 表示一个空类型,而 nullundefined 都是一个具有意义的实际类型。因此undefined 能够被赋值给 void 类型的变量,就像这样:

const voidVar1: void = undefined;

// null 也可以,但是需要关闭 strictNullChecks
const voidVar2: void = null;
1
2
3
4

# 数组的类型标注

# 数组

数组同样是最常用的类型之一,在 TypeScript 中有两种方式来声明一个数组类型:

const arr1: string[] = [];

const arr2: Array<string> = [];
1
2
3

这两种方式是完全等价的,但其实更多是以前者为主。

# 元组

在某些情况下,使用元组(Tuple)来代替数组要更加妥当,比如一个数组中只存放固定长度的变量,但我们进行了超出长度的访问。如果是数组,会抛出错误;使用元素的话,能在越界访问时给出类型报错:

// 使用数组,运行时报错
const arr3: string[] = ['apple', 'pear', 'banana'];
console.log(arr3[599]);

// 使用元组,在越界访问时会给出类型报错
const arr4: [string, string, string] = ['apple', 'pear', 'banana'];
console.log(arr4[599]);
1
2
3
4
5
6
7

在写法上,元祖类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同:

因为在 JavaScript 中并没有元组的概念,所以 TypeScript 的数组和元组在转译为 JavaScript 后都是数组,但元组最重要的特性是数量固定,类型可以各异。

同时,为了增加可读性,在 TypeScript 4.0 中,有了具名元组 (opens new window)的支持,使得我们可以为元组中的元素打上类似 label 的标记:

const arr7: [name: string, age: number, male: boolean] = ['张三', 13, true];
1

# 对象的类型标注

对象的类型标注是比较重要的部分,因为它在 JavaScript 中也是使用最频繁的数据结构。

在 TypeScript 中,用来描述对象类型的类型标注是 interface,可以理解为它代表了这个对象对外提供的接口结构(好吧,如果会后端语言,会觉得给「对象」被「接口」多少还是有点区别的,不过这是前端嘛)。

# 声明方式

首先使用 interface 声明一个结构,然后使用这个结构来作为一个对象的类型标注即可:

// 声明一个接口
interface IDescription {
  name: string;
  age: number;
  male: boolean;
}

// 用上面声明的接口,给一个对象标注类型
const obj1: IDescription = {
  name: '张三',
  age: 13,
  male: true,
};
1
2
3
4
5
6
7
8
9
10
11
12
13

这里声明的接口表示:

  • 对象每一个属性的值必须一一对应到接口的属性类型。
  • 不能有多的属性,也不能有少的属性,包括直接在对象内部声明,或是 obj1.other = 'xxx' 这样属性访问赋值的形式。

除了声明属性以及属性的类型以外,我们还可以对属性进行修饰,常见的修饰包括可选(Optional)与只读(Readonly)这两种。

# 修饰接口属性

在接口结构可以通过 ? 来标记一个属性为可选:

interface IDescription {
  name: string;
  age: number;
  male?: boolean;
  func?: Function;
}

const obj2: IDescription = {
  name: '张三',
  age: 13,
  male: true,
  // 无需实现 func 也是合法的
};
1
2
3
4
5
6
7
8
9
10
11
12
13

在这种情况下,即使在 obj2 中定义了 male 属性,但当你访问 obj2.male 时,它的类型仍然会是 boolean | undefined

同理,即使对上面的可选属性(func 函数)进行了赋值,然后调用(obj2.func()),TypeScript 仍然会以接口的描述为准进行类型检查,此时将会产生一个类型报错:「不能调用可能是未定义的方法」。但可选属性标记不会影响你对这个属性进行赋值和实际使用,要想解决这个类型验证的问题,可以使用类型断言、非空断言或可选链(后面再说)。

除了标记一个属性为可选以外,还可以标记这个属性为只读:readonly。它的作用是防止对象的属性被再次赋值。

interface IDescription {
  readonly name: string;
  age: number;
}

const obj3: IDescription = {
  name: '张三',
  age: 13,
};

// 无法分配到 "name" ,因为它是只读属性
obj3.name = "张三";
1
2
3
4
5
6
7
8
9
10
11
12

其实在数组与元组层面也有着只读的修饰,但与对象类型有着两处不同:

  • 只能将整个数组/元组标记为只读,而不能像对象那样标记某个属性为只读。
  • 一旦被标记为只读,那这个只读数组/元组的类型上,将不再具有 pushpop 等方法(即会修改原数组的方法),因此报错信息也将是「类型 xxx 上不存在属性 "push"」这种。这一实现的本质是只读数组与只读元组的类型实际上变成了 ReadonlyArray,而不再是 Array

# type 与 interface

在 TypeScript 中,type(类型别名)和 interface(接口)都可以用来描述对象,在大多数情况下效果等价。不过还是推荐:

  • interface 用来描述对象、类的结构。
  • type 用来将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型。(这也更贴合「别名」这一称谓)

但大部分场景下接口都可以被类型别名所取代,因此,只要你觉得统一使用类型别名让你觉得更整齐,也没什么问题。

# object、Object 以及 {}

objectObject 以及 {}(一个空对象)这三者的使用也是挺让人困惑的,这里区分一下吧。

首先是 Object 的使用。在 JavaScript 原型链的知识中提到,原型链的顶端是 Object 以及 Function,这也就意味着所有的原始类型与对象类型最终都指向 Object,在 TypeScript 中就表现为 Object 包含了所有的类型:

// 对于 undefined、null、void 0 ,需要关闭 strictNullChecks
const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;

const tmp4: Object = 'apple';
const tmp5: Object = 599;
const tmp6: Object = { name: '张三' };
const tmp7: Object = () => {};
const tmp8: Object = [];
1
2
3
4
5
6
7
8
9
10

Object 类似的还有 BooleanNumberStringSymbol,这几个装箱类型(Boxed Types)同样包含了一些超出预期的类型。以 String 为例,它同样包括 undefinednullvoid,以及代表的拆箱类型(Unboxed Types)string,但并不包括其他装箱类型对应的拆箱类型,如 boolean 与 基本对象类型,看以下的代码:

const tmp9: String = undefined;
const tmp10: String = null;
const tmp11: String = void 0;
const tmp12: String = 'apple';

// 以下不成立,因为不是字符串类型的拆箱类型
const tmp13: String = 599; // 报错
const tmp14: String = { name: '张三' }; // 报错
const tmp15: String = () => {}; // 报错
const tmp16: String = []; // 报错
1
2
3
4
5
6
7
8
9
10

注意:在任何情况下,都不应该使用这些装箱类型

object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数类型这些:

const tmp17: object = undefined;
const tmp18: object = null;
const tmp19: object = void 0;

const tmp20: object = 'apple';  // 报错,值为原始类型
const tmp21: object = 599;        // 报错,值为原始类型

const tmp22: object = { name: '张三' };
const tmp23: object = () => {};
const tmp24: object = [];
1
2
3
4
5
6
7
8
9
10

最后是 {},可以认为它就是一个对象字面量类型(字面量类型后面再讲)。即使用 {} 作为类型标注就是一个合法的,但内部无属性定义的空对象,它意味着任何非 null / undefined 的值:

虽然能够将其作为任意变量的类型,但实际上却无法对这个变量进行任何赋值操作

const tmp30: {} = { name: '张三' };

tmp30.age = 18; // 报错 类型 "{}" 上不存在属性 "age"
1
2
3

这是因为它就是纯洁的像一张白纸一样的空对象,上面没有任何的属性(除了 toString 这种与生俱来的)。

结论

  • 在任何时候都不要使用 Object 以及类似的装箱类型(少学一样会带来困扰的东西,心理负担会小很多)。
  • 同样要避免使用 {}。它意味着任何非 null / undefined 的值,从这个层面上看,这和使用 any 一样恶劣。
  • 当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用 object。但更推荐进一步区分,也就是使用 Record<string, unknown>Record<string, any> 表示对象,unknown[]any[] 表示数组,(...args: any[]) => any 表示函数这样。

# 总结

这一章是衔接 JS 和 TS 两者的过渡,可以认为是学完 JS,如何带着 JS 的基础来学 TS。其实可以分为两类:

  • 与 JavaScript 概念基本一致的部分,如原始类型与数组类型需要重点掌握,但因为思维方式基本没有变化,所以可以认为就是在写更严格一些的 JavaScript。
  • 一些全新的概念,比如元组与 readonly 修饰等,这一部分就是 JS 没有的东西了,需要稍微转换一下思维方式。

通过这一章的过渡,后面就完全进入 TS 独有的世界了。

(完)