推荐一下小册 TypeScript 全面进阶指南,此篇笔记来源于此,记录总结,加深印象!
另外,如果想了解更多ts相关知识,可以参考我的其他笔记:
除了最常见的 number / string / boolean / null / undefined, ES6、ES11)又分别引入了 2 个新的原始类型:symbol 与 bigint
const name: string = 'ts';
const age: number = 18;
const male: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = { name, age, male };
const bigintVar1: bigint = 90071992547409212n;
const bigintVar2: bigint = BigInt(9007199254740991);
const symbolVar: symbol = Symbol('unique');
在没有开启strictNullChecks 检查的情况下,这两种类型会被视作其他类型的子类型,比如 string 类型会被认为包含了 null 与 undefined 类型:
const tmp1: null = null;
const tmp2: undefined = undefined;
const tmp3: string = null; // 仅在关闭 strictNullChecks 时成立,下同
const tmp4: string = undefined;
用于描述一个内部没有 return 语句,或者没有显式 return 一个值的函数的返回值,如:
function func1() {}
function func2() {
return;
}
function func3() {
return undefined;
}
const arr1: string[] = []; // 更推荐此写法
const arr2: Array<string> = [];
元组就是类型和数据的个数一开始就已经限定好了,某些情况下使用元组代替数组要更加妥当。
下面是一些应用
提供越界提醒
const arr3: string[] = ['aaa', 'bbb', 'ccc']
const arr4: [string,string,string] = ['aaa', 'bbb', 'ccc']
console.log(arr4[4])
可选
const arr6: [string, number?, boolean?] = ['aaa'];
// 下面这么写也可以
// const arr6: [string, number?, boolean?] = ['aaa', , ,];
具名元组
在 TypeScript 4.0 中,有了具名元组(Labeled Tuple Elements)的支持,使得我们可以为元组中的元素打上类似属性的标记:
const arr7: [name: string, age: number, male?: boolean] = ['aaa', 18, true];
interface IDescription {
name: string;
age: number;
male: boolean;
}
const obj1: IDescription = {
name: 'aaa',
age: 599,
male: true,
};
可选 ?
interface IDescription {
name: string;
age: number;
male?: boolean;
func?: Function;
}
const obj2: IDescription = {
name: 'aaa',
age: 599,
male: true,
// 无需实现 func 也是合法的
};
只读
interface IDescription {
readonly name: string;
age: number;
}
const obj3: IDescription = {
name: 'aaa',
age: 599,
};
// 无法分配到 "name" ,因为它是只读属性
obj3.name = "AAA";
type User = {
name: string
age: number
};
很多人更喜欢用 type(Type Alias,类型别名)来代替接口结构描述对象,
而更推荐的方式是
但大部分场景下接口结构都可以被类型别名所取代,取决于个人喜好或者团队的一些规范吧。
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 = 'aaa';
const tmp5: Object = 599;
const tmp6: Object = { name: 'aaa' };
const tmp7: Object = () => {};
const tmp8: Object = [];
和 Object 类似的还有 Boolean、Number、String、Symbol,这几个装箱类型(Boxed Types) 同样包含了一些超出预期的类型。
以 String 为例,它同样包括 undefined、null、void,以及代表的 拆箱类型(Unboxed Types) —> string,但并不包括其他装箱类型对应的拆箱类型,如 boolean 与 基本对象类型,我们看以下的代码:
const tmp9: String = undefined;
const tmp10: String = null;
const tmp11: String = void 0;
const tmp12: String = 'aaa';
// !!! 以下不成立,因为不是字符串类型的拆箱类型
const tmp13: String = 599;
const tmp14: String = { name: 'aaa' };
const tmp15: String = () => {};
const tmp16: String = [];
object
object 的引入就是为了解决对 Object 类型的错误使用,它代表所有非原始类型的类型,即数组、对象与函数类型这些:
const tmp17: object = undefined;
const tmp18: object = null;
const tmp19: object = void 0;
const tmp20: object = 'aaa'; // X 不成立,值为原始类型
const tmp21: object = 599; // X 不成立,值为原始类型
const tmp22: object = { name: 'aaa' };
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: {} = 'aaa';
const tmp29: {} = 599;
const tmp30: {} = { name: 'aaa' };
const tmp31: {} = () => {};
const tmp32: {} = [];
虽然能够将其作为变量的类型,但你实际上无法对这个变量进行任何赋值操作:
const tmp30: {} = { name: 'aaa' };
tmp30.age = 18; // X 类型“{}”上不存在属性“age”。
总结
在任何时候都不要,不要,不要使用 Object 以及类似的装箱类型。
当你不确定某个变量的具体类型,但能确定它不是原始类型,可以使用
object。但更推荐进一步区分,也就是使用
Record<string, unknown> 或 Record<string, any> 表示对象,
unknown[] 或 any[] 表示数组,
(…args: any[]) => any表示函数 这样。
我们同样要避免使用{}。{}意味着任何非 null / undefined 的值,从这个层面上看,使用它和使用 any 一样恶劣。
看一个例子,我们开发中定义的请求 接口返回 结果可能如下
interface Res {
code: 10000 | 10001 | 50000;
status: "success" | "failure";
data: any;
}
上面"success" 或者 “failure” 不是一个值吗?为什么它也可以作为类型?
在 TypeScript 中,这叫做字面量类型(Literal Types),它代表着比原始类型更精确的类型,同时也是原始类型的子类型
字面量类型主要包括:字符串字面量类型、数字字面量类型、布尔字面量类型和对象字面量类型,它们可以直接作为类型标注:
const str: "aaa" = "aaa";
const num: 599 = 599;
const bool: true = true
// 报错!Type '"aaa123"' is not assignable to type '"aaa"'
const str1: "aaa" = "aaa123";
单独使用字面量类型比较少见,因为单个字面量类型并没有什么实际意义。
它通常和联合类型(即这里的 |)一起使用,表达一组字面量类型:
interface Tmp {
bool: true | false;
num: 1 | 2 | 3;
str: "aa" | "bb" | "cc"
}
补充:联合类型
联合类型你可以理解为,它代表了一组类型的可用集合,只要最终赋值的类型属于联合类型的成员之一,就可以认为符合这个联合类型。
联合类型对其成员并没有任何限制,除了上面这样对同一类型字面量的联合,我们还可以将各种类型混合到一起:
interface Tmp {
mixed: true | string | 599 | {} | (() => {}) | (1 | 2)
}
联合类型的常用场景之一是通过多个对象类型的联合,来实现手动的互斥属性,即这一属性如果有字段1,那就没有字段2:
interface Tmp {
user:
| {
vip: true;
expires: string;
}
| {
vip: false;
promotion: string;
};
}
declare var tmp: Tmp;
if (tmp.user.vip) {
console.log(tmp.user.expires);
}
// js中定义一些常量
export const PageUrl = {
Home_Page_Url: "url1",
Setting_Page_Url: "url2",
Share_Page_Url: "url3",
}
使用ts的枚举 enum
enum PageUrl {
Home_Page_Url = "url1",
Setting_Page_Url = "url2",
Share_Page_Url = "url3",
}
// 使用
const home = PageUrl.Home_Page_Url;
如果你没有声明枚举的值,它会默认使用数字枚举,并且从 0 开始,以 1 递增:
enum Items {
Foo,
Bar,
Baz
}
// Items.Foo , Items.Bar , Items.Baz的值依次是 0,1,2
如果你只为某一个成员指定了枚举值,那么之前未赋值成员仍然会使用从 0 递增的方式,之后的成员则会开始从枚举值递增。
enum Items {
Foo, // 0
Bar = 599,
Baz // 600
}
枚举和对象的重要差异在于,对象是单向映射的,我们只能从键映射到键值。
而枚举是双向映射的,即你可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员:
enum Items {
Foo,
Bar,
Baz
}
const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"
// 函数声明
function foo(name: string): number {
return name.length;
}
// 函数表达式
const foo = function (name: string): number {
return name.length
}
const foo: (name: string) => number = function (name) {
return name.length
}
// 箭头函数
// 方式一
const foo = (name: string): number => {
return name.length
}
// 方式二 代码的可读性会非常差,不推荐
const foo: (name: string) => number = (name) => {
return name.length
}
要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来,如下:
type FuncFoo = (name: string) => number
const foo: FuncFoo = (name) => {
return name.length
}
如果只是为了描述这个函数的类型结构,我们甚至可以使用 interface 来进行函数声明:
interface FuncFooStruct {
(name: string): number
}
void 类型
// 没有调用 return 语句
function foo(): void { }
// 调用了 return 语句,但没有返回值
function bar(): void {
return;
}
可选参数与 rest 参数
使用 ? 描述一个可选参数
注意:可选参数必须位于必选参数之后
// 在函数逻辑中注入可选参数默认值
function foo1(name: string, age?: number): number {
const inputAge = age || 18; // 或使用 age ?? 18
return name.length + inputAge
}
// 直接为可选参数声明默认值
function foo2(name: string, age: number = 18): number {
const inputAge = age;
return name.length + inputAge
}
对于 rest 参数的类型标注也比较简单,由于其实际上是一个数组,这里我们也应当使用数组类型进行标注:
function foo(arg1: string, ...rest: any[]) { }
// 元组标注
function foo(arg1: string, ...rest: [number, boolean]) { }
foo("aaa", 18, true)
重载
在某些逻辑较复杂的情况下,函数可能有多组入参类型和返回值类型:
function func(foo: number, bar?: boolean): string | number {
if (bar) {
return String(foo);
} else {
return foo * 599;
}
}
要想实现与入参关联的返回值类型,我们可以使用 TypeScript 提供的函数重载签名(Overload Signature),将以上的例子使用重载改写:
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;
}
}
const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number
异步函数
async function asyncFunc(): Promise<void> {}
对于异步函数(即标记为 async 的函数),其返回值必定为一个 Promise 类型,而 Promise 内部包含的类型则通过泛型的形式书写,即 Promise < T >
一个函数的主要结构即是参数、逻辑和返回值,对于逻辑的类型标注其实就是对普通代码的标注,所以我们只介绍了对参数以及返回值的类型标注。
而到了 Class 中其实也一样,它的主要结构只有构造函数、属性、方法和访问符(Accessor),我们也只需要关注这三个部分即可
构造函数
class Foo {
prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
print(addon: string): void {
console.log(`${this.prop} and ${addon}`)
}
get propA(): string {
return `${this.prop}+A`;
}
set propA(value: string) {
this.prop = `${value}+A`
}
}
!!setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。
修饰符
分别有 public / private / protected / readonly
readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)
其它三个属于访问性修饰符
class Foo {
private prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
protected print(addon: string): void {
console.log(`${this.prop} and ${addon}`)
}
public get propA(): string {
return `${this.prop}+A`;
}
public set propA(value: string) {
this.propA = `${value}+A`
}
}
抽象类
抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等),一个抽象方法描述了这一方法在实际实现中的结构。
abstract class AbsFoo {
abstract absProp: string;
abstract get absGetter(): string;
abstract absMethod(name: string): string
}
抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员,如这里的抽象方法。我们可以实现(implements)一个抽象类:
class Foo implements AbsFoo {
absProp: string = "aaa"
get absGetter() {
return "aaa"
}
absMethod(name: string) {
return name
}
}
另外使用 interface 不仅可以声明函数结构,也可以声明类的结构:
interface FooStruct {
absProp: string;
get absGetter(): string;
absMethod(input: string): string
}
class Foo implements FooStruct {
absProp: string = "aaa"
get absGetter() {
return "aaa"
}
absMethod(name: string) {
return name
}
}
any 类型的主要意义,其实就是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能够被所有类型兼容。
千万不要 anyscript !!!
unknown 类型的变量可以再次赋值为任意其它类型,但只能赋值给 any 与 unknown 类型的变量
let unknownVar: unknown = "aaa";
unknownVar = false;
unknownVar = "aaa";
unknownVar = {
site: "bbb"
};
unknownVar = () => { }
const val1: string = unknownVar; // Error
const val2: number = unknownVar; // Error
const val3: () => {} = unknownVar; // Error
const val4: {} = unknownVar; // Error
const val5: any = unknownVar;
const val6: unknown = unknownVar;
any 放弃了所有的类型检查,而 unknown 并没有
let unknownVar: unknown;
unknownVar.foo(); // 报错:对象类型为 unknown
// 类型断言
let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo();
类型断言: 虽然这是一个未知的类型,但我跟你保证它在这里就是这个类型!
never 类型被称为 Bottom Type,是整个类型系统层级中最底层的类型
never 类型不携带任何的类型信息,因此会在联合类型中被直接移除
可以理解为一个接受参数的函数
type Factory<T> = T | number | string;
type中的泛型大多是用来进行工具类型封装,比如
// 把一个对象中的所有属性变成 string类型
type Stringify<T> = {
[K in keyof T]: string;
};
// 复制对象中的所有属性类型
type Clone<T> = {
[K in keyof T]: T[K];
};
看一个例子
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface IFoo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type PartialIFoo = Partial<IFoo>;
// 等价于
interface PartialIFoo {
prop1?: string;
prop2?: number;
prop3?: boolean;
prop4?: () => void;
}
泛型还可以作为条件类型中的判断条件
type IsEqual<T> = T extends true ? 1 : 2;
type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'linbudu'>; // 2
可以设置默认值
type Factory<T = boolean> = T | number | string;
// 调用时就可以不带任何参数了,默认会使用我们声明的默认值来填充
const foo: Factory = false;
可以使用 extends 关键字来约束传入的泛型参数必须符合要求
比如:
看下面例子,根据传入的请求码判断请求是否成功
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"
type Res3 = ResStatus<'10000'>; // 类型“string”不满足约束“number”。
type ResStatus<ResCode extends number = 10000> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res4 = ResStatus; // "success"
多泛型关联
不仅可以同时传入多个泛型参数,还可以让这几个泛型参数之间也存在联系,类似于传入多个参数
type Conditional<Type, Condition, TruthyResult, FalsyResult> =
Type extends Condition ? TruthyResult : FalsyResult;
// "passed!"
type Result1 = Conditional<'aaa', string, 'passed!', 'rejected!'>;
// "rejected!"
type Result2 = Conditional<'bbb', boolean, 'passed!', 'rejected!'>;
常见的一个例子应该还是响应类型结构的泛型处理:
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
这个接口描述了一个通用的响应类型结构,预留出了实际响应数据的泛型坑位,然后在你的请求函数中就可以传入特定的响应类型了:
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}
type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}
函数中的泛型是很常用的,主要是做:类型的自动提取
function handle<T>(input: T): T {}
我们为函数声明了一个泛型参数 T,并将参数的类型与返回值类型指向这个泛型参数。这样,在这个函数接收到参数时,T 会自动地被填充为这个参数的类型。
例子:
function handle<T>(input: T): T {}
const author = "aaa"; // 使用 const 声明,被推导为 "aaa"
let authorAge = 18; // 使用 let 声明,被推导为 number
handle(author); // 填充为字面量类型 "aaa"
handle(authorAge); // 填充为基础类型 number
在基于参数类型进行填充泛型时,其类型信息会被推断到尽可能精确的程度,如这里会推导到字面量类型而不是基础类型。
这是因为在直接传入一个值时,这个值是不会再被修改的,因此可以推导到最精确的程度。而如果你使用一个变量作为参数,那么只会使用这个变量标注的类型(在没有标注时,会使用推导出的类型)。
例子:
function swap<T, U>([start, end]: [T, U]): [U, T] {
return [end, start];
}
const swapped1 = swap(["aaa", 18]); // const swapped1: [number, string]
const swapped2 = swap([null, 18]); // const swapped2: [number, null]
const swapped3 = swap([{ name: "aaa" }, {}]); // const swapped3: [{}, { name: string;}]
函数中的泛型同样存在约束与默认值
// 不再处理对象类型的情况了
function handle<T extends string | number>(input: T): T {}
// 只想处理数字元组的情况
function swap<T extends number, U extends number>([start, end]: [T, U]): [U, T] {
return [end, start];
}
函数的泛型参数也会被内部的逻辑消费,如:
function handle<T>(payload: T): Promise<[T]> {
return new Promise<[T]>((res, rej) => {
res([payload]);
});
对于箭头函数的泛型,其书写方式是这样的:
const handle = <T>(input: T): T => {}
在 tsx 文件中泛型的尖括号可能会造成报错,编译器无法识别这是一个组件还是一个泛型,此时你可以让它长得更像泛型一些:
const handle = <T extends any>(input: T): T => {}
需要注意的是,不要为了用泛型而用泛型,就像这样:
没有意义
function handle<T>(arg: T): void {
console.log(arg);
};
Class 中的泛型和函数中的泛型非常类似,只不过函数中泛型参数的消费方是参数和返回值类型
Class 中的泛型消费方则是属性、方法、乃至装饰器等。
同时 Class 内的方法还可以再声明自己独有的泛型参数。我们直接来看完整的示例:
class Queue<TElementType> {
private _list: TElementType[];
constructor(initial: TElementType[]) {
this._list = initial;
}
// 入队一个队列泛型子类型的元素
enqueue<TType extends TElementType>(ele: TType): TElementType[] {
this._list.push(ele);
return this._list;
}
// 入队一个任意类型元素(无需为队列泛型子类型)
enqueueWithUnknownType<TType>(element: TType): (TElementType | TType)[] {
return [...this._list, element];
}
// 出队
dequeue(): TElementType[] {
this._list.shift();
return this._list;
}
}