typescript 学习笔记
前言
TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准。
TypeScript 由微软开发的自由和开源的编程语言。
TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。
1.原始类型与对象类型
null
与 undefined
当未开启 strictNullChecks
时,null
和 undefined
可以作为其他类型的子类型
void
void
表示一个空类型,而 null
与 undefined
都是一个具有意义的实际类型
数组的类型标注
这两种方式完全等价,推荐使用string[]
const arr:string[] = []
const arr:Array<string> = []
type
与 interface
type:
将一个函数签名、一组联合类型、一个工具类型等等抽离成一个完整独立的类型。
interface:
描述对象、类的结构
Object 、object
以及 {}
Objcet:
原型链的顶端是 Object
以及 Function
,这也就意味着所有的原始类型与对象类型最终都指向 Object
,在 TypeScript
中就表现为 Object
包含了所有的类型
object
: 代表所有非原始类型的类型,即数组、对象与函数类型
const tmp22: object = { name: 'wskang' };
const tmp23: object = () => {};
const tmp24: object = [];
{}:
{}
就是一个对象字面量类型 ,或者叫 内部无属性定义的空对象 , 可以表示任何非 null / undefined
的值。使用它和使用any一样恶劣
注:在任何时候都不要使用Object以及类似的装箱类型。
2.字面量类型与枚举
字面量类型:直接使用字符串和数字等做类型, 类型主要包括字符串字面量类型、数字字面量类型、布尔字面量类型和对象字面量类型
联合类型: 代表了一组类型的可用集合
interface Tmp {mixed: true | string | test | {} | (() => {}) | (1 | 2)
}
枚举
将需要的常量约束在一个命名空间下,枚举默认的值是从0开始,依次递增。
延迟求值的枚举值
使用条件: 如果你使用了延迟求值,那么没有使用延迟求值的枚举成员必须放在使用常量枚举值声明的成员之后,或者放在第一位:
const returnNum = () => 100 + 499;enum Items {Foo = returnNum(),Bar = test,Baz
}
当枚举值为数字时,枚举是双向映射的
enum Items {Foo,Bar,Baz
}const fooValue = Items.Foo; // 0
const fooKey = Items[0]; // "Foo"
常量枚举: 常量枚举和枚举相似,只是其声明多了一个 const
, 对于常量枚举,只能通过枚举成员访问枚举值(而不能通过值访问成员)
3.函数与Class
中的类型
函数
函数的类型描述了 函数入参类型与函数返回值类型 ,
主要结构
- 参数
- 逻辑
- 返回值
// 函数声明
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
}// 如果是单纯描述函数的类型结构,可以用interface,interface 就是用来描述一个类型结构的,而函数类型本质上也是一个结构固定的类型罢了。
interface FuncFooStruct {(name: string): number
}
函数重载
// 重载签名
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 * test;}
}const res1 = func(test); // number
const res2 = func(test, true); // string
const res3 = func(test, false); // number
Class
主要结构
- 构造函数
- 属性
- 方法
- 访问符
类修饰符
-
public:
访问性修饰符, 此类成员在类、类的实例、子类中都能被访问。 -
private:
访问性修饰符, 此类成员仅能在类的内部被访问。 -
protected:
访问性修饰符, 此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员。 -
readonly:
操作性修饰符,
一个小技巧—— 可以在构造函数中对参数应用访问性修饰符 , 参数会被直接作为类的成员(即实例的属性),免去后续的手动赋值。
class Foo {constructor(public arg1: string, private arg2: boolean) { }
}new Foo("wskang", true)
override
关键字
当基类中没有override指定的方法,派生类会报错
class Base {printWithLove() { }
}class Derived extends Base {override print() {// ...}
}
抽象类: 一个抽象类描述了一个类中应当有哪些成员(属性、方法等),一个抽象方法描述了这一方法在实际实现中的结构。
abstract class AbsFoo {abstract absProp: string;abstract get absGetter(): string;abstract absMethod(name: string): string
}class Foo implements AbsFoo {absProp: string = "wskang"get absGetter() {return "wskang"}absMethod(name: string) {return name}
}
注: 在 TypeScript
中无法声明静态的抽象成员。
同样的描述结构,也可以使用interface, interface 不仅可以声明函数结构,也可以声明类的结构
interface FooStruct {absProp: string;get absGetter(): string;absMethod(input: string): string
}class Foo implements FooStruct {absProp: string = "wskang"get absGetter() {return "wskang"}absMethod(name: string) {return name}
}
4. any、unknown、never
与类型断言
any:
表示一个无拘无束的“任意类型”,它能兼容所有类型,也能够被所有类型兼容。 也是ts语言中的顶级类型。
尽量避免any,请记住以下几点
- 如果是类型不兼容报错导致你使用 any,考虑用类型断言替代,我们下面就会开始介绍类型断言的作用。
- 如果是类型太复杂导致你不想全部声明而使用 any,考虑将这一处的类型去断言为你需要的最简类型。如你需要调用
foo.bar.baz()
,就可以先将 foo 断言为一个具有 bar 方法的类型。 - 如果你是想表达一个未知类型,更合理的方式是使用 unknown。
unknown:
表示一个 unknown 类型的变量可以再次赋值为任意其它类型,但只能赋值给 any 与 unknown 类型的变量 ,层级和any一致
let unknownVar: unknown = "wskang";unknownVar = false;
unknownVar = "wskang";
unknownVar = {site: "csdn"
};unknownVar = () => { }const val1: string = unknownVar; // Error
const val2: number = unknownVar; // Error
const val3: () => {} = unknownVar; // Error
const val4: {} = unknownVar; // Errorconst val5: any = unknownVar;
const val6: unknown = unknownVar;
never:
代表一个“什么都没有”的类型,它甚至不包括空的类型,严格来说,never
类型不携带任何的类型信息,因此会在联合类型中被直接移除 ,是ts语言中的最底层的类型。
一旦一个返回值类型为 never 的函数被调用,那么下方的代码都会被视为无效的代码(即无法执行到)
function justThrow(): never {throw new Error()
}function foo (input:number){if(input > 1){justThrow();// 等同于 return 语句后的代码,即 Dead Codeconst name = "wskang";}
}
类型断言:将类型强制为我们命名的类型
实现作为代码提示的辅助工具,就可以在保留类型提示的前提下,不那么完整地实现这个结构
interface IStruct {foo: string;bar: {barPropA: string;barPropB: number;barMethod: () => void;baz: {handler: () => Promise<void>;};};
}// 一堆错误
const obj: IStruct = {};// 这个例子是不会报错的
const obj = <IStruct>{bar: {baz: {},},
};
5.类型工具(上)
在类型别名中,类型别名可以这么声明自己能够接受泛型(我称之为泛型坑位)。一旦接受了泛型,我们就叫它工具类型:
type Factory<T> = T | number | string;
// 一般不会直接使用工具类型来做类型标注,而是再度声明一个新的类型别名
type FactoryWithBool = Factory<boolean>;
const foo: FactoryWithBool = true;
对于工具类型来说,它的主要意义是基于传入的泛型进行各种类型操作,得到一个新的类型。
交叉类型
类似&&,不过在ts中是&,需要满足所有类型
type Res11 = string & number & boolean // never
索引类型
包含三个部分: 索引签名类型、索引类型查询与索引类型访问
索引签名类型
索引签名类型主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构
interface AllStringTypes {[key: string]: string;
}type AllStringTypes = {[key: string]: string;
}
索引查询类型
keyof
操作符。严谨地说,它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型
interface Foo {wskang: 1,test: 2
}type FooKeys = keyof Foo; // "wskang" | test
// 在 VS Code 中悬浮鼠标只能看到 'keyof Foo'
// 看不到其中的实际值,你可以这么做:
type FooKeys = keyof Foo & {}; // "wskang" | test// 类似js代码
type FooKeys = Object.keys(Foo).join(" | ");
索引类型访问
索引类型查询的本质其实就是,通过键的字面量类型('propA'
)访问这个键对应的键值类型(number
)。
interface Foo {propA: number;propB: boolean;
}type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean// 配合keyof操作符
interface Foo {propA: number;propB: boolean;propC: string;
}type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean
注: 在未声明索引签名类型的情况下,我们不能使用 NumberRecord[string]
这种原始类型的访问方式,而只能通过键名的字面量类型来进行访问
interface Foo {propA: number;
}// 类型“Foo”没有匹配的类型“string”的索引签名。
type PropAType = Foo[string];
映射类型
映射类型的主要作用即是基于键名映射到键值类型
type Stringify<T> = {[K in keyof T]: string; // 映射类型(即这里的 in 关键字)
};interface Foo {prop1: string;prop2: number;prop3: boolean;prop4: () => void;
}type StringifiedFoo = Stringify<Foo>;// 等价于
interface StringifiedFoo {prop1: string;prop2: string;prop3: string;prop4: string;
}// 克隆
type Clone<T> = {[K in keyof T]: T[K];
};// 这里的T[K]其实就是上面说到的索引类型访问,我们使用键的字面量类型访问到了键值的类型,这里就相当于克隆了一个接口。需要注意的是,这里其实只有K in 属于映射类型的语法,keyof T 属于 keyof 操作符,[K in keyof T]的[]属于索引签名类型,T[K]属于索引类型访问。
6.类型工具(下)
typescript
中的typeof
可以直接在类型标注中使用 typeof,还能在工具类型中使用 typeof
const func = (input: string) => {return input.length > 10;
}const func2: typeof func = (name: string) => {return name === 'wskang'
}
typeof 返回的类型就是当你把鼠标悬浮在变量名上时出现的推导后的类型,并且是最窄的推导程度(即到字面量类型的级别)。
类型守卫
TypeScript
中提供了非常强大的类型推导能力,它会随着你的代码逻辑不断尝试收窄类型,这一能力称之为类型的控制流分析(也可以简单理解为类型推导)。
类型控制流分析做不到跨函数上下文来进行类型的信息收集(但别的类型语言中可能是支持的), 为了解决这一类型控制流分析的能力不足, TypeScript
引入了 is 关键字来显式地提供类型信息
function isString(input: unknown): boolean {return typeof input === "string";
}function foo(input: string | number) {if (isString(input)) {// 类型“string | number”上不存在属性“replace”。(input).replace("wskang", "wskangtest")}if (typeof input === 'number') { }// ...
}// 添加is关键字
function isString(input: unknown): input is string {return typeof input === "string";
}function foo(input: string | number) {if (isString(input)) {// 正确了(input).replace("wskang", "wskangtest")}if (typeof input === 'number') { }// ...
}
is string
,即 is 关键字 + 预期类型,即如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到。
in
与 instanceof
in:
就像对象key in object
一样,用来判断一个类型是否存在
instanceof:
会沿着原型链查找是否在其上
7.泛型
泛型就类似于函数传参一样, 类型别名中的泛型大多是用来进行工具类型封装
type Stringify<T> = {[K in keyof T]: string;
};type Clone<T> = {[K in keyof T]: T[K];
};
条件类型
在条件类型参与的情况下,通常泛型会被作为条件类型中的判断条件(T extends Condition
,或者 Type extends T
)以及返回值(即 :
两端的值),这也是我们筛选类型需要依赖的能力之一
泛型约束
使用 extends
关键字来约束传入的泛型参数必须符合要求 。 A extends B
意味着 A 是 B 的子类型
- 字面量类型是对应原始类型的子类型
- 联合类型子集均为联合类型的子类型
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”。
注:对象、函数、class和内置方法的使用请参考小册,不过大同小异
多泛型关联
多泛型参数其实就像接受更多参数的函数,其内部的运行逻辑(类型操作)会更加抽象,表现在参数(泛型参数)需要进行的逻辑运算(类型操作)会更加复杂。
type Conditional<Type, Condition, TruthyResult, FalsyResult> =Type extends Condition ? TruthyResult : FalsyResult;// "passed!"
type Result1 = Conditional<'wskang', string, 'passed!', 'rejected!'>;// "rejected!"
type Result2 = Conditional<'wskang', boolean, 'passed!', 'rejected!'>;
8.结构化类型系统
class Cat {eat() { }
}class Dog {eat() { }
}function feedCat(cat: Cat) { }// 神奇的传不是Cat类型也不会报错,这是为什么?
feedCat(new Dog())
Cat 与 Dog 类型上的方法是一致的,所以它们虽然是两个名字不同的类型,但仍然被视为结构一致,这就是结构化类型系统的特性。你可能听过结构类型的别称鸭子类型(*Duck Typing*),这个名字来源于鸭子测试(*Duck Test*)。其核心理念是,如果你看到一只鸟走起来像鸭子,游泳像鸭子,叫得也像鸭子,那么这只鸟就是鸭子。
标称类型系统
标称类型系统(Nominal Typing System)要求,两个可兼容的类型,其名称必须是完全一致的。
类型的重要意义之一是限制了数据的可用操作与实际意义,这一点在标称类型系统中的体现要更加明显。
对于标称类型系统,父子类型关系只能通过显式的继承来实现,称为标称子类型。
在 TypeScript 中模拟标称类型系统
详情见小册案例好理解
9.类型系统层级
- 字面量类型 < 对应的原始类型
- 字面量类型 < 包含此字面量类型的联合类型,原始类型 < 包含此原始类型的联合类型
- 同一基础类型的字面量联合类型 < 此基础类型
- 字面量类型 < 包含此字面量类型的联合类型(同一基础类型) < 对应的原始类型
- 原始类型 < 原始类型对应的装箱类型 < Object 类型
- Object < any / unknown
- never < 字面量类型
10.类型里的逻辑运算
infer
关键字
TypeScript
中支持通过 infer
关键字来在条件类型中提取类型的某一部分信息
type FunctionReturnType<T extends Func> = T extends (...args: any[]
) => infer R? R: never;
infer
,意为推断,如 infer R
中 R
就表示 待推断的类型。
分布式条件类型
分布式条件类型,也称条件类型的分布式特性,只不过是条件类型在满足一定情况下会执行的逻辑而已 。
分布式起作用的条件:
- 你的类型参数需要是一个联合类型
- 类型参数需要通过泛型参数的方式传入,而不能直接进行条件类型判断
- 条件类型中的泛型参数不能被包裹
条件类型分布式特性会产生的效果很明显,即将这个联合类型拆开来,每个分支分别进行一次条件类型判断,再将最后的结果合并起来 。
type Intersection<A, B> = A extends B ? A : never;type IntersectionRes = Intersection<1 | 2 | 3, 2 | 3 | 4>; // 2 | 3
当条件类型的判断参数为 any,会直接返回条件类型两个结果的联合类型。而在这里其实类似,当通过泛型传入的参数为 never,则会直接返回 never。
一个小技巧
// 判断是否any
type IsAny<T> = 0 extends 1 & T ? true : false; // 如果交叉类型的其中一个成员是 any,此时最终类型必然是 any// 判断是否unknown
type IsUnknown<T> = unknown extends T? IsAny<T> extends true? false: true: false;