ts小册学习之 类型-爱代码爱编程
JS 内置的原始类型在 TypeScript 中它们都有对应的类型注解。其中,除了 null 与 undefined 以外,余下的类型对应 JavaScript 中的数据类型概念。
🍽️ null 与 undefined
在 TypeScript 中,null 与 undefined 类型都是有具体意义的类型。
这两者在没有开启 strictNullChecks
检查的情况下,会被视作其他类型的子类型。
🥚 void
在 TypeScript 中的 void 和 JavaScript 中的 void 不一样。void 用于描述函数没有显式 return 返回值,可以认为 void 表示一个空类型(null 与 undefined 都是具有意义的实际类型)。
🍻 type 与 interface
interface 用来描述对象、类的结构,而类型别名用来将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型。
🍢 object、Object 以及 { }
- Object,JS 原型链的顶端是 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 = 'linbudu';
const tmp5: Object = 599;
const tmp6: Object = { name: 'linbudu' };
const tmp7: Object = () => {};
const tmp8: Object = [];
和 Object 类似的还有 Boolean、Number、String、Symbol,这几个装箱类型(Boxed Types) 同样包含了一些超出预期的类型。以 String 为例,它同样包括 undefined、null、void,以及代表的 拆箱类型(Unboxed Types) string,但并不包括其他装箱类型对应的拆箱类型,如 boolean 与 基本对象类型。
在任何情况下,你都不应该使用这些装箱类型。
- object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数类型这些:
const tmp17: object = undefined;
const tmp18: object = null;
const tmp19: object = void 0;
const tmp20: object = 'linbudu'; // X 不成立,值为原始类型
const tmp21: object = 599; // X 不成立,值为原始类型
const tmp22: object = { name: 'linbudu' };
const tmp23: object = () => {};
const tmp24: object = [];
{}
,一个空对象,可以认为{}
就是一个对象字面量类型(对应到字符串字面量类型这样)。你可以认为使用{}
作为类型签名就是一个合法的,但内部无属性定义的空对象,这类似于 Object(想想new Object()
),它意味着任何非 null / undefined 的值:
const tmp25: {} = undefined; // 仅在关闭 strictNullChecks 时成立,下同
const tmp26: {} = null;
const tmp27: {} = void 0; // void 0 等价于 undefined
const tmp28: {} = 'linbudu';
const tmp29: {} = 599;
const tmp30: {} = { name: 'linbudu' };
const tmp31: {} = () => {};
const tmp32: {} = [];
虽然能够将其作为变量的类型,但你实际上无法对这个变量进行任何赋值操作。
🍥 字面量类型
字面量类型主要包括字符串字面量类型、数字字面量类型、布尔字面量类型和对象字面量类型,它们可以直接作为类型标注。它代表着比原始类型更精确的类型,同时也是原始类型的子类型。它通常和联合类型(即这里的 |
)一起使用,表达一组字面量类型。
例子:
interface Tmp {
mixed: true | string | 599 | {} | (() => {}) | (1 | 2)
}
这里有几点需要注意的:
- 对于联合类型中的函数类型,需要使用括号
()
包裹起来 - 函数类型并不存在字面量类型,因此这里的
(() => {})
就是一个合法的函数类型 - 你可以在联合类型中进一步嵌套联合类型,但这些嵌套的联合类型最终都会被展平到第一级中
对象字面量类型
对象字面量类型就是一个对象类型的值。当然,这也就意味着这个对象的值全都为字面量值:
interface Tmp {
obj: {
name: "linbudu",
age: 18
}
}
const tmp: Tmp = {
obj: {
name: "linbudu",
age: 18
}
}
需要注意的是,无论是原始类型还是对象类型的字面量类型,它们的本质都是类型而不是值。它们在编译时同样会被擦除,同时也是被存储在内存中的类型空间而非值空间。
如果说字面量类型是对原始类型的进一步扩展,那么枚举在某些方面则可以理解为是对对象类型的扩展。
🍭 枚举
如果要和 JavaScript 中现有的概念对比,最贴切的可能就是曾经写过的 constants 文件了。
enum PageUrl {
Home_Page_Url = "url1",
Setting_Page_Url = "url2",
Share_Page_Url = "url3",
}
const home = PageUrl.Home_Page_Url;
这么做的好处非常明显。首先,你拥有了更好的类型提示。其次,这些常量被真正地约束在一个命名空间下。
枚举和对象的重要差异在于,对象是单向映射的,我们只能从键映射到键值。而枚举是双向映射的,即你可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员:
enum Items {
Foo,
Bar,
Baz
}
const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"
要了解这一现象的本质,我们需要来看一看枚举的编译产物,如以上的枚举会被编译为以下 JavaScript 代码:
"use strict";
var Items;
(function (Items) {
Items[Items["Foo"] = 0] = "Foo";
Items[Items["Bar"] = 1] = "Bar";
Items[Items["Baz"] = 2] = "Baz";
})(Items || (Items = {}));
obj[k] = v
的返回值即是 v,因此这里的 obj[obj[k] = v] = k
本质上就是进行了 obj[k] = v
与 obj[v] = k
这样两次赋值。
但需要注意的是,仅有值为数字的枚举成员才能够进行这样的双向枚举,字符串枚举成员仍然只会进行单次映射。
除了数字枚举与字符串枚举这种分类以外,其实还存在着普通枚举与常量枚举这种分类方式。
常量枚举和枚举相似,只是其声明多了一个 const:
const enum Items { Foo, Bar, Baz }
const fooValue = Items.Foo; // 0
它和普通枚举的差异主要在访问性与编译产物。对于常量枚举,你只能通过枚举成员访问枚举值(而不能通过值访问成员)。同时,在编译产物中并不会存在一个额外的辅助对象(如上面的 Items 对象),对枚举成员的访问会被直接内联替换为枚举的值。以上的代码会被编译为如下形式:
const fooValue = 0 /* Foo */; // 0
实际上,常量枚举的表现、编译产物还受到配置项
--isolatedModules
以及--preserveConstEnums
等的影响,我们会在后面的 TSConfig 详解中了解更多。
🪖 函数
函数重载签名
拥有多个重载声明的函数在被调用时,是按照重载的声明顺序往下查找。
function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
if (bar) {
return String(foo);
} else {
return foo * 599;
}
}
异步函数、Generator 函数等类型签名
async function asyncFunc(): Promise<void> {}
function* genFunc(): Iterable<void> {}
async function* asyncGenFunc(): AsyncIterable<void> {}
🐸 Class
访问符:public
/ private
/ protected
/ readonly
,protected:此类成员仅能在类与子类中被访问。
静态成员
class Foo {
static staticHandler() { }
public instanceHandler() { }
}
翻译成es5
var Foo = /** @class */ (function () {
function Foo() {}
// 静态成员不处于原型链
Foo.staticHandler = function () { };
// 方法和属性处于原型链
Foo.prototype.instanceHandler = function () { };
return Foo;
}());
静态成员直接被挂载在函数体上,而实例成员挂载在原型上。
静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类)。而原型对象上的实例成员则会沿着原型链进行传递,也就是能够被继承。
Extend
class Base { }
class Derived extends Base { }
派生类中可以访问到使用 public
或 protected
修饰符的基类成员。基类中的方法也可以在派生类中被覆盖。
但仍然可以通过 super 访问到基类中的方法:
class Base {
print() { }
}
class Derived extends Base {
print() {
super.print()
// ...
}
}
在派生类中覆盖基类方法时,并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?
所以,TypeScript 4.3 新增了 override
关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义:
class Base {
printWithLove() { }
}
class Derived extends Base {
override print() {
// ...
}
}
抽象类
抽象类是对类结构与方法的抽象。我们可以实现(implements)一个抽象类,且必须完全实现这个抽象类的每一个抽象成员。
在 TypeScript 中无法声明静态的抽象成员。
抽象类和 interface 的区别
TS 的 interface 是一种规范,不存在于运行时,只存在于类型空间。
抽象类是只为了类服务的,并且在运行时也会存在,存在于值空间的类型描述与约束,它的本质是一个合法的类。