TypeScript 类型系统——从原始类型到函数签名

本文为「TypeScript 深入理解」系列第 1 篇,后续文章将分别覆盖面向对象与泛型、装饰器与编译原理、实战技巧与模式。

JavaScript 的动态类型在项目初期是优势——写起来快,改起来随意。但代码量上来之后,同一个灵活性就变成了技术债务:改一个函数签名,调用方不会收到任何提示,你只能跑一遍测试或者等线上报错来发现遗漏。

TypeScript 在 JavaScript 之上加了一层编译时的类型检查。类型不匹配、属性拼写错误、参数遗漏——这些问题在构建阶段就能被拦截,而不是等到浏览器控制台报错。

类型注解带来的不止是安全网。它本身就是活的文档:看一个函数签名就知道入参和返回值,不需要翻源码或读注释。

类型注解:给变量加上约束

JavaScript 原始类型也同样适应于 TypeScript 的类型系统,stringnumberboolean 可以直接当作类型注解使用:

原始类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let num: number;
let str: string;
let bool: boolean;

num = 123;
num = 123.456;
num = '123'; // Error

str = '123';
str = 123; // Error

bool = true;
bool = false;
bool = 'false'; // Error

TypeScript 为数组提供了专用的类型语法。后缀 [] 配合类型注解(如 :boolean[]),既能让你安全地操作数组,也能防止赋值错误类型的成员:

数组

1
2
3
4
5
6
7
8
9
10
11
12
let boolArray: boolean[];

boolArray = [true, false];
console.log(boolArray[0]); // true
console.log(boolArray.length); // 2

boolArray[1] = true;
boolArray = [false, false];

boolArray[0] = 'false'; // Error
boolArray = 'false'; // Error
boolArray = [true, 'false']; // Error

JavaScript 没有原生的元组,通常只能用数组模拟。TypeScript 补上了这个缺口,使用 :[typeofmember1, typeofmember2] 语法即可定义定长定类型的元组:

元组类型

1
2
3
4
5
6
7
let nameNumber: [string, number];

// Ok
nameNumber = ['Jenny', 221345];

// Error
nameNumber = ['Jenny', '221345'];

特殊类型:any、unknown、void、never

any

any 是类型系统的「后门」——一旦你用 any 注解一个变量,TypeScript 对它的类型检查就完全关闭了。所有类型都能赋值给 anyany 也能赋值给任何类型。换句话说,用了 any 就等于回到了纯 JavaScript。

null 和 undefined

在类型系统中,JavaScript 中的 null 和 undefined 字面量和其他被标注了 any 类型的变量一样,都能被赋值给任意类型的变量,如下例子所示:

1
2
3
4
5
6
7
8
// strictNullChecks: false 关闭严格的 null 检查

let num: number;
let str: string;

// 这些类型能被赋予
num = null;
str = undefined;

void

使用 :void 来表示一个函数没有一个返回值

never

never 类型是 TypeScript 中的底层类型,表示永远不会发生的类型。它自然出现的场景:

  • 一个永远不会有返回值的函数(如:while(true) {} 无限循环);
  • 一个总是会抛出错误的函数(如:function foo() { throw new Error('Not Implemented') }foo 的返回类型是 never);

你也可以将它用做类型注解:

1
let foo: never; // ok

但是,never 类型仅能被赋值给另外一个 never

1
2
3
4
5
6
let foo: never = 123; // Error: number 类型不能赋值给 never 类型

// ok, 作为函数返回类型的 never
let bar: never = (() => {
throw new Error('Not Implemented');
})();

unknown

unknown 是 TypeScript 3.0 引入的类型,它是 any 的「安全版」。与 any 不同,unknown 类型的值不能直接赋值给其他类型,也不能直接调用方法——你必须先进行类型收窄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let value: unknown;

// 可以赋值任何类型给 unknown
value = 'hello';
value = 42;
value = { name: 'Alice' };

// 但不能直接使用
// value.toUpperCase(); // Error: Object is of type 'unknown'
// let str: string = value; // Error

// 必须先做类型判断
if (typeof value === 'string') {
let str: string = value; // ok,类型已收窄
console.log(value.toUpperCase()); // ok
}

// 或者用类型断言
(value as string).toUpperCase();

能用 unknown 就不要用 anyunknown 保留了类型安全,迫使你在使用前做检查。

接口:给对象形状下定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// interface object
interface User {
name: string
age?: number
// readonly id: number
}
function printUser(user: User): void{
console.log(user.name, ':', user.age);
}
const user1: User = {
name: '小刘',
age: 23,
};
printUser(user1);

//interface function 函数类型
interface UserFun {
(name: string, age: number): void;
}

const myFunction:UserFun = (name, age)=>{
console.log('SetUser');
console.log(name, ':', age);
}

myFunction('1', 2);

函数:给签名加上类型保障

函数的类型注解可以约束参数和返回值。在 TypeScript 中,函数类型有多种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 直接注解参数和返回值
function add(x: number, y: number): number {
return x + y;
}

// 2. 用类型别名定义函数类型
type AddFn = (x: number, y: number) => number;
const add2: AddFn = (x, y) => x + y;

// 3. 用 interface 定义函数类型
interface AddFn2 {
(x: number, y: number): number;
}

可选参数和默认值:

1
2
3
4
5
6
7
8
9
// 可选参数必须放在必选参数后面
function greet(name: string, greeting?: string): string {
return `${greeting || 'Hello'}, ${name}!`;
}

// 默认参数
function greet2(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}

剩余参数使用 ... 展开:

1
2
3
4
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6

函数重载允许同一个函数名根据参数类型的不同执行不同逻辑。TypeScript 要求先声明重载签名,再提供统一实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 重载签名
function format(value: number): string;
function format(value: Date): string;
// 统一实现
function format(value: number | Date): string {
if (typeof value === 'number') {
return value.toFixed(2);
}
return value.toISOString();
}

console.log(format(3.14159)); // '3.14'
console.log(format(new Date())); // '2022-01-03T...'

掌握了类型注解、接口和函数类型,你已经能应对大部分日常开发场景。下一篇将进入 TypeScript 的面向对象特性——类、泛型、枚举、迭代器,以及继承、多态和抽象类。