从 C、Python 等传统语言走过来的对类型都有“深刻”的理解,去掉枚举、列表、元祖、集合等组合型的,基础类型我整理了下表:
类型 | C | Python | Go | typescript |
---|---|---|---|---|
整数 | int short long | int long | int[x] uint[x] rune | number |
浮点数 | float double | float | float32 float64 | number |
复数 | _complex | complex | complex64 complex128 | number |
字符 | char | byte | ||
字符串 | str unicode | string | string | |
布尔 | _bool | boolean | bool | boolean |
指针 | * | uintptr | ||
任意类型 | any unknown | |||
无类型 | void | NoneType | void never | |
特色类型 | 字面量类型 |
ts 比其他语言在某些方面好像简化了,比如用 number 代替各种数字型,但某些方面又好像复杂了很多,任意类型和无类型就弄出来 4 种,还搞出来 字面量类型 —— 这些变化,体现了 ts 语言设计者的 权衡和用心,同时语言设计者也为用户扩展 ts 的 type 留下了一些语法,更是在 type 自定义、扩展这个方面与其他语言拉开了差异。
下面,从我一个刚入门不久的 ts 用户经历和感受,聊聊我最近对 TypeScript 中 Type 这几个字的理解。
类型名是否添加
python 和 go 在代码中类型名是可加可不加的,大部分情况下都不需要用户自己加,而是编译器自己推导,而 ts 走了 C 的路线,十分甚至强制用户指明所有类型。
比如定义变量:
- Python:
i = 10
或i: int = 10
- Go:
i := 10
或i int := 10
- C :
int i = 10
- ts:
const i:number = 10
从定义方便性上看,python 和 go 都替用户节省了键盘敲击,ts 学习了几十年前的老大哥 C,决定毁掉用户键盘。
why?—— 不是我今天详聊的内容,略过。
ts 中的任意类型:any、unknown 及变量 undefined
- any:任意类型的变量
- unknown: 表示未知类型,unknown 与 any 类似 但使用前必须进行断言或守卫
- undefined: 是个内建变量,不是类型
any 和 unknown 的相同点是:变量可以被赋值任何类型的值
let anyTypeVar: any;
anyTypeVar = "foobar";
anyTypeVar = 10;
anyTypeVar = { foobar: 10 };
let unknownTypeVar: unknown;
unknownTypeVar = "foobar";
unknownTypeVar = 10;
unknownTypeVar = { foobar: 10 };
any 和 unknown 的不同点是:any 变量可以向其他类型变量赋值,unkonwn 却只能向 any 和 unknown 变量赋值。
let i: number = 10;
i = anyTypeVar; // OK,并且 i 不再是 number 类型,而是 any 类型
// i = unknownTypeVar; // Error,语法 不允许
anyTypeVar = unknownTypeVar; // OK
所以,any 可以理解成双向的,能进能出;unknown 是个单向的,出口只能内循环。
ts 中的空虚类型:never、void 及变量 null
- never: 永不存在的值的类型
- void: 无任何类型,没有类型,常用于表示函数没有返回值
- null: 是个内建变量,不是类型
never 比较特殊,可以做函数返回值,但只能放在不能正常结束的函数里面,比如:死循环、抛异常……
function loopForever(msg: string): never {
while (true) {}
}
function error(msg: string): never {
throw new Error(msg);
}
never 也可以定义变量,但任何类型都不能赋值给 never 类型(除了 never 本身之外,any 也不行)。
void 常用与函数返回值
function foobar(): void {
console.log("hello world");
}
相比不写 void 做返回值,好处是一旦这个函数哪天被某个程序员不小心写了个返回值,是会报错的。
void 类型的变量只能赋值 undefined、null、和 void
let v1: void;
// v1 = 10 // Error
v1 = null;
v1 = undefined;
let v2: void;
v1 = v2;
ts 类型之于防御型和契约型
防御型:即无法限制接收的数据类型、数据量时,只能自己内部做好各级防范措施的业务场景,求人不如求己型。—— 将入参声明为 any、unknown 可以做到,并且节省函数重载等方式带来的复杂性。
比如:使用 typeof(类型保护)和 as(类型断言)。
举 例:函数入参不做限制,但 string、number 不同类型做不同处理。
function foobar(notSure: unknown) {
if (typeof notSure === "string") {
console.log((notSure as string).toLowerCase());
}
if (typeof notSure === "number") {
console.log(notSure);
}
}
foobar("ABC"); // abc
foobar(10); // 10
契约型:即双方严格遵守 API 定义的业务场景,白纸黑字、签字画押型。 —— 细致、明确的 API 才能支撑这种业务场景,口头和文档都约束不了 100% 的,只有语言级别的才能彻底解决。
比如上面例子中去掉 unknown,指定的更详细些:
function foobar(notSure: string|number): void {
...
}
如果契约是这么制定的:foobar 的返回值也要是 string | number
,可以
function foobar(notSure: string|number): string|number {
...
}
或者用泛型
function foobar<T>(notSure: T): T {
...
}
foobar<string>('ABC')
foobar<number>(10)
foobar<boolean>(true) // 也是允许的,怎么避免?
不希望的 boolean 也是可以的,怎么避免?
function foobar<T extends string|number>(notSure: T): T {
...
}
foobar<string>('ABC')
foobar<number>(10)
// foobar<boolean>(true) // Error
契约好像能够描述的更清晰和确定了,不错。C、Python 具备这样的契约描述能力么?—— 我觉得暂时还不能,尤其是我们继续看下面越来越复杂的契约。