typescript成长之路
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上。
它的第一个版本发布于 2012 年 10 月,经历了多次更新后,现在已成为前端社区中不可忽视的力量,不仅在 Microsoft 内部得到广泛运用,而且 Google 开发的 Angular 从 2.0 开始就使用了 TypeScript 作为开发语言,Vue 3.0 也使用 TypeScript 进行了重构。
学习方法:
先快速过,可标记疑难点,必须回顾所学,分析总结,转化成自己的理解与应用。
快速入门
为什么推荐去官方文档查看
第三方文档可能不够细,甚至断章取义,原本一个容易理解的概念,第三方文档解释就变了味道。
常用命令
1 | # 输出ts帮助,即指令用法 |
常用依赖
ts-node
简化 typescript 运行步骤,可直接在 node.js 环境中执行 ts 代码。
官网描述:用于 node.js 的 TypeScript 执行和 REPL,具有源映射和本机 ESM 支持。
当然,如果适用vscode开发,可以使用Code Runner插件,选择需要运行的文件,鼠标右键选择Run Code即可执行编译输出。
nodemon
监控 node.js 应用程序中的任何更改并自动重启服务器 - 非常适合开发。
在package.json中配置如下
1 | { |
tsconfig.json配置
打包辅助工具
parcel
html中引入ts,可编译打包成js。
webpack
自定义打包辅助工具
重点记忆
有关功能的更多信息
函数重载
类型操作
从类型中创建类型
- 泛型 - 带参数的类型
- Keyof 类型操作符- keyof 操作符创建新类型
- Typeof 类型操作符 - 使用 typeof 操作符来创建新的类型
- 索引访问类型 - 使用 Type[‘a’] 语法来访问一个类型的子集
- 条件类型 - 在类型系统中像if语句一样行事的类型
- 映射类型 - 通过映射现有类型中的每个属性来创建类型
- 模板字面量类型 - 通过模板字面字符串改变属性的映射类型
泛型
泛型是一种通用的编程概念,它允许在编写代码时不指定具体的类型,而是在使用代码时提供类型。通过使用泛型,可以编写更加通用和灵活的代码,使其能够适应不同的数据类型和数据结构。
在 TypeScript 中,泛型通常使用尖括号 < >
包裹,后跟一个标识符,例如 T
、U
、K
等等。这个标识符可以在代码中用作类型注释或泛型函数或类的参数。
例如,下面是一个泛型函数的例子,它使用类型参数 T
来表示一个数组中元素的类型,并返回数组中所有元素的和:
1 | function sum<T>(numbers: T[]): T { |
在这个例子中,sum
函数接受一个类型为 T
的数组,并返回类型为 T
的值。当我们使用 sum
函数时,需要提供一个实际的类型作为类型参数,例如:
1 | const numbers = [1, 2, 3, 4, 5]; |
在这个例子中,我们将 numbers
数组作为参数传递给 sum
函数,并在调用函数时使用 number
类型作为类型参数。函数将返回一个 number
类型的值,它是数组中所有元素的和。
通过使用泛型,我们可以编写可重用的代码,使其能够适应不同的数据类型和数据结构,从而提高代码的灵活性和通用性。
什么是类型参数
泛型也可以称为类型参数。在 TypeScript 中,泛型可以被用作类型参数,用于指定在编写代码时不确定的数据类型。
因此,泛型和类型参数的概念是相互关联的。泛型是一种通用的编程概念,用于指定在编写代码时不确定的数据类型,而类型参数则是指在使用泛型时需要提供的具体类型。
使用泛型类型变量
首先,让我们做一下泛型的 “ hello world”:身份函数。身份函数是一个函数,它将返回传入的任何内容。你可以用类似于echo命令的方式来考虑它。
如果没有泛型,我们将不得不给身份函数一个特定的返回值类型。
1 | function identity(arg: number): number { |
或者,我们可以用任意类型来描述身份函数。
1 | function identity(arg: any): any { |
使用 any 当然是通用的,因为它将使函数接受 arg 类型的任何和所有的类型。实际上我们在函数返回时失去了关于该类型的信息。如果我们传入一个数字,我们唯一的信息就是任何类型都可以被返回。
相反,我们需要一种方法来捕获参数的类型,以便我们也可以用它来表示返回的内容。在这里,我们将使用一个类型变量,这是一种特殊的变量,对类型而不是数值起作用。
1 | function identity<Type>(arg: Type): Type { |
不借助编译器的类型推断写法
1 | let output = identity<string>("myString"); |
相反,编译器只是查看了 “myString “这个值,并将Type设置为其类型。
1 | let output = identity("myString"); |
如果我们想在每次调用时将参数 arg 的长度记录到控制台,该怎么办?我们可能很想这样写:
1 | function loggingIdentity<Type>(arg: Type): Type { |
当我们这样做时,编译器会给我们一个错误,说我们在使用 arg 的 .length 成员,但我们没有说arg 有这个成员。记住,我们在前面说过,这些类型的变量可以代表任何和所有的类型,所以使用这个函数的人可以传入一个 number ,而这个数字没有一个 .length 成员。
比方说,我们实际上是想让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。既然我们在处理数组,那么 .length 成员应该是可用的。我们可以像创建其他类型的数组那样来描述它。
1 | function loggingIdentity<Type>(arg: Type[]): Type[] { |
你可以把 loggingIdentity 的类型理解为 “通用函数 loggingIdentity 接收一个类型参数 Type 和
一个参数 arg , arg 是一个 Type 数组,并返回一个 Type 数组。” 如果我们传入一个数字数组,我们会得到一个数字数组,因为Type会绑定到数字。这允许我们使用我们的通用类型变量 Type 作为我们正在处理的类型的一部分,而不是整个类型,给我们更大的灵活性。
我们也可以这样来写这个例子:
1 | function loggingIdentity<Type>(arg: Array<Type>): Array<Type> { |
泛型类型
在前面的部分中,我们创建了适用于一系列类型的通用身份函数。在本节中,我们将探讨函数本身的类型以及如何创建通用接口。
泛型函数的类型与非泛型函数的类型一样,首先列出类型参数,类似于函数声明:
1 | function identity<Type>(arg: Type): Type { |
我们也可以为类型中的泛型类型参数使用不同的名称,只要类型变量的数量和类型变量的使用方式一致即可。
1 | function identity<Type>(arg: Type): Type { |
我们还可以将泛型类型写成对象字面量类型的调用签名:
1 | function identity<Type>(arg: Type): Type { |
感觉有种匿名函数的写法,怪怪的。
这导致我们编写了第一个通用接口。让我们把前面例子中的对象字面量移到一个接口中:
1 | interface GenericIdentityFn { |
在类似的示例中,我们可能希望将通用参数移动为整个接口的参数。这让我们可以看到我们通用的类型(例如,Dictionary<string>
而不仅仅是Dictionary
)。这使得类型参数对接口的所有其他成员可见。
1 | interface GenericIdentityFn<Type> { |
请注意,我们的示例已更改为略有不同。我们现在没有描述泛型函数,而是有一个非泛型函数签名,它是泛型类型的一部分。当我们使用GenericIdentityFn
时,我们现在还需要指定相应的类型参数(此处:number
),有效地锁定底层调用签名将使用的内容。了解何时将类型参数直接放在调用签名上以及何时将其放在接口本身上将有助于描述类型的哪些方面是通用的。
除了泛型接口,我们还可以创建泛型类。请注意,无法创建通用枚举和命名空间。
泛型类
类和接口一样,可以是泛型的。当一个泛型类用new实例化时,其类型参数的推断方式与函数调用的
方式相同。
1 | class Box<Type> { |
类可以像接口一样使用通用约束和默认值。
静态成员中的类型参数。
1 | class Box<Type> { |
请记住,类型总是被完全擦除的! 在运行时,只有一个Box.defaultValue属性。这意味着设置Box.defaultValue(如果有可能的话)也会改变Box.defaultValue,这可不是什么好事。一个泛型类的静态成员永远不能引用该类的类型参数。
泛型约束
在我们的loggingIdentity
示例中,我们希望能够访问.length
的属性arg
,但编译器无法证明每个类型都有一个.length
属性,因此它警告我们不能做出这种假设。
1 | function loggingIdentity<Type>(arg: Type): Type { |
我们不想使用任何和所有类型,而是希望将此函数限制为使用也 具有该.length
属性的任何和所有类型。只要类型有这个成员,我们就允许它,但它至少需要有这个成员。为此,我们必须将我们的要求列为限制条件Type
。
为此,我们将创建一个描述约束的接口。在这里,我们将创建一个具有单个.length
属性的接口,然后我们将使用该接口和extends
关键字来表示我们的约束:
1 | interface Lengthwise { |
因为泛型函数现在受到约束,所以它不再适用于所有类型:
1 | loggingIdentity(3); // error 类型“number”的参数不能赋给类型“Lengthwise”的参数。 |
相反,我们需要传入其类型具有所有必需属性的值:
1 | loggingIdentity({ length: 10, value: 3 }); |
在泛型约束中使用类型参数
您可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从给定名称的对象中获取属性。我们想确保我们不会意外获取obj
上不存在的属性,因此我们将在两种类型之间放置一个约束:
1 | function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) { |
在泛型中使用类类型
在 TypeScript 中使用泛型创建工厂时,需要通过构造函数来引用类类型。例如,
1 | function create<Type>(c: { new (): Type }): Type { |
一个更高级的示例使用原型属性来推断和约束构造函数与类类型的实例端之间的关系。
1 | class BeeKeeper { |
此模式用于为混合设计模式提供动力。
类型keyof
运算符
运算keyof
符采用对象类型并生成其键的字符串或数字文字联合。以下类型 P 与“x” | ”y“是同一类型:
1 | type Point = { x: number; y: number }; |
如果类型有一个string
或number
索引签名,keyof
将返回这些类型:
1 | type Arrayish = { [n: number]: unknown }; |
请注意,在此示例中,M
是 string | number
, 这是因为 JavaScript 对象键始终被强制转换为字符串,因此obj[0]
始终转为obj["0"]
.
keyof
类型在与映射类型结合使用时变得特别有用,我们稍后将详细了解这一点。
类型运算符
JavaScript 已经有一个typeof
可以在表达式上下文中使用的运算符:
1 | // Prints "string" |
TypeScript 添加了一个typeof
运算符,您可以在类型上下文中使用它来引用变量或属性的类型:
1 | let s = "hello"; |
这对基本类型不是很有用,但结合其他类型运算符,可以typeof
方便地表达许多模式。例如,让我们从查看预定义类型开始ReturnType<T>
,它接受一个函数类型并产生它的返回类型:
1 | type Predicate = (x: unknown) => boolean; |
ReturnType
如果我们尝试在函数名称上使用,我们会看到一个指示性错误:
1 | function f() { |
请记住,值和类型不是一回事。要引用值的类型,我们使用:f typeof
1 | function f() { |
限制
TypeScript 有意限制了你可以使用的表达式种类typeof
。
typeof
具体来说,只有在标识符(即变量名)或其属性上使用才是合法的。这有助于避免编写您认为正在执行但实际上不是的代码的混乱陷阱:
1 | function msgbox() {} |
索引访问类型
我们可以使用索引访问类型来查找另一种类型的特定属性:
1 | type Person = { age: number; name: string; alive: boolean }; |
索引类型本身就是一种类型,因此我们可以keyof
完全使用联合、 或其他类型:
1 | type I1 = Person["age" | "name"]; // type I1 = string | number |
如果您尝试索引一个不存在的属性,您甚至会看到一个错误:
1 | type I1 = Person["alve"]; // error 类型“Person”上不存在属性“alve”。 |
使用任意类型进行索引的另一个示例是使用number
获取数组元素的类型。我们可以将它与typeof
结合起来以方便地捕获数组文字的元素类型:
1 | const MyArray = [ |
您只能在索引时使用类型,这意味着您不能使用 const
来进行变量引用:
1 | const key = "age"; |
但是,您可以为类似风格的重构使用类型别名:
1 | type key = "age"; |
条件类型
在最有用的程序的核心,我们必须根据输入做出决定。JavaScript 程序没有什么不同,但考虑到值可以很容易地自省这一事实,这些决定也基于输入的类型。 条件类型有助于描述输入和输出类型之间的关系。
1 | interface Animal { |
条件类型的形式有点像JavaScript 中的条件表达式 (condition ? trueExpression : falseExpression
):
1 | SomeType extends OtherType ? TrueType : FalseType; |
当左侧的类型extends
可分配给右侧的类型时,您将获得第一个分支(“真实”分支)中的类型;否则你会在后一个分支(“false”分支)中得到类型。
从上面的示例中,条件类型可能不会立即看起来有用 - 我们可以告诉自己是否Dog extends Animal
选择number
or string
!但条件类型的强大之处在于将它们与泛型一起使用。
例如,让我们采用以下createLabel
功能:
1 | interface IdLabel { |
createLabel 的这些重载描述了一个 JavaScript 函数,该函数根据其输入的类型做出选择。注意几点:
- 如果一个库必须在其 API 中一遍又一遍地做出相同类型的选择,这将变得很麻烦。
- 我们必须创建三个重载:一个用于我们确定类型的每种情况(一个用于
string
,一个用于number
),一个用于最一般的情况(采用 astring | number
)。对于每个可以处理的新类型createLabel
,重载的数量呈指数增长。
相反,我们可以将该逻辑编码为条件类型:
1 | type NameOrId<T extends number | string> = T extends number |
然后我们可以使用该条件类型将我们的重载简化为没有重载的单个函数。
1 | function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { |
条件类型约束
通常,条件类型的检查会为我们提供一些新信息。就像使用类型保护进行缩小可以为我们提供更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步限制泛型。
例如,让我们采取以下内容:
1 | type MessageOf<T> = T["message"]; // error 类型“"message"”无法用于索引类型“T”。 |
在此示例中,TypeScript 出错是因为T
不知道有一个名为 的属性message
。我们可以约束T
,TypeScript 将不再抱怨:
1 | type MessageOf<T extends { message: unknown }> = T["message"]; |
但是,如果我们想MessageOf
采用任何类型,并默认为某个属性不可用never
时怎么办?message
我们可以通过移出约束并引入条件类型来做到这一点:
1 | type MessageOf<T> = T extends { message: unknown } ? T["message"] : never; |
在 true 分支中,TypeScript 知道T
将有一个message
属性。
作为另一个示例,我们还可以编写一个名为Flatten
的类型,将数组类型展平为它们的元素类型,但除此之外别管它们:
1 | type Flatten<T> = T extends any[] ? T[number] : T; |
当Flatten
给定一个数组类型时,它使用索引number
访问来获取 的string[]
元素类型。否则,它只返回给定的类型。
在条件类型中进行推断
我们只是发现自己使用条件类型来应用约束,然后提取类型。这最终成为一种常见的操作,条件类型使它变得更容易。
条件类型为我们提供了一种方法,可以使用infer
关键字从我们在真实分支中比较的类型进行推断。例如,我们可以推断元素类型Flatten
而不是使用索引访问类型“手动”取出它:
1 | type Flatten<Type> = Type extends Array<infer Item> ? Item : Type; |
在这里,我们使用infer
关键字声明性地引入一个新的泛型类型变量 named而不是指定如何在 true 分支中Item
检索元素类型。T
这使我们不必考虑如何深入挖掘和剖析我们感兴趣的类型的结构。
我们可以使用关键字编写一些有用的辅助类型别名infer
。例如,对于简单的情况,我们可以从函数类型中提取返回类型:
1 | type GetReturnType<Type> = Type extends (...args: never[]) => infer Return |
当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,将根据最后一个签名进行推断(这大概是最宽松的包罗万象的情况)。不可能根据参数类型列表执行重载决策。
1 | declare function stringOrNum(x: string): number; |
或者
1 | function stringOrNum(x: string): number; |
Math.random()
是随机的,const t1: T1 = true
可有可能error 不能将类型“boolean”分配给类型“number”
。
分布式条件类型
当条件类型作用于泛型类型时,它们在给定联合类型时变得具有分配性。例如,采用以下内容:
1 | type ToArray<Type> = Type extends any ? Type[] : never; |
如果我们将联合类型插入到 中ToArray
,则条件类型将应用于该联合的每个成员。
1 | type ToArray<Type> = Type extends any ? Type[] : never; |
这里发生的是StrArrOrNumArr
分布在:
1 | string | number; |
并将联合的每个成员类型映射到有效的:
1 | ToArray<string> | ToArray<number>; |
这给我们留下了:
1 | string[] | number[]; |
通常,分配性是所需的行为。extends
为避免这种行为,您可以用方括号将关键字的每一侧括起来。
1 | type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; |
映射类型
当您不想重复自己时,有时一种类型需要基于另一种类型。
映射类型建立在索引签名的语法之上,用于声明未提前声明的属性类型:
1 | type OnlyBoolsAndHorses = { |
映射类型是一种通用类型,它使用PropertyKeys
的联合(通常通过 keyof
创建)来遍历键以创建类型:
1 | type OptionsFlags<Type> = { |
在此示例中,OptionsFlags
将从类型中获取所有属性Type
并将它们的值更改为布尔值。
1 | type FeatureFlags = { |
映射修饰符
在映射期间可以应用两个额外的修饰符:readonly
和?
分别影响可变性和可选性。
您可以通过前缀-
或+
来删除或添加这些修饰符。如果您不添加前缀,则假定为+
。
1 | // Removes 'readonly' attributes from a type's properties |
1 | // Removes 'optional' attributes from a type's properties |
键重映射通过as
在 TypeScript 4.1 及更高版本中,您可以使用映射类型中的子句as
重新映射映射类型中的键:
1 | type MappedTypeWithNewProperties<Type> = { |
您可以利用模板字面量类型等功能从先前的属性名称创建新的属性名称:
1 | type Getters<Type> = { |
您可以通过条件类型生成never
来过滤掉键:
1 | // Remove the 'kind' property |
您可以映射任意联合,不仅string | number | symbol
的联合,还可以是任何类型的联合:
1 | type EventConfig<Events extends { kind: string }> = { |
进一步探索
映射类型与此类型操作部分中的其他功能配合得很好,例如,这里是一个使用条件类型的映射类型,它返回 true
或false
取决于对象是否将属性pii
设置为字面意义上的true
:
1 | type ExtractPII<Type> = { |
模板文字类型
模板字面量类型建立在字符串字面量类型之上,并且能够通过联合扩展为多个字符串。
它们与 JavaScript 中的模板文字字符串具有相同的语法,但用于类型位置。当与具体文字类型一起使用时,模板文字通过连接内容生成新的字符串文字类型。
1 | type World = "world"; |
当在插值位置使用联合时,类型是每个联合成员可以表示的每个可能字符串文字的集合:
1 | type EmailLocaleIDs = "welcome_email" | "email_heading"; |
对于模板字面量中的每个插值位置,并集交叉相乘:
1 | type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; |
我们通常建议人们对大型字符串联合使用提前生成,但这在较小的情况下很有用。
类型中的字符串联合
模板字面量类型建立在字符串字面量类型之上,并且能够通过联合扩展为多个字符串。
它们与 JavaScript 中的模板文字字符串具有相同的语法,但用于类型位置。当与具体文字类型一起使用时,模板文字通过连接内容生成新的字符串文字类型。
1 | type World = "world"; |
当在插值位置使用联合时,类型是每个联合成员可以表示的每个可能字符串文字的集合:
1 | type EmailLocaleIDs = "welcome_email" | "email_heading"; |
对于模板字面量中的每个插值位置,并集交叉相乘:
1 | type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; |
我们通常建议人们对大型字符串联合使用提前生成,但这在较小的情况下很有用。
类型中的字符串联合
当基于类型中的信息定义新字符串时,模板字面量的威力就体现出来了。
考虑这样一种情况,函数 ( makeWatchedObject
) 添加一个新函数on()
调用给传递的对象。在 JavaScript 中,它的调用可能如下所示 makeWatchedObject(baseObject)
:我们可以想象基础对象看起来像:
1 | const passedObject = { |
on
将添加到基础对象的函数需要两个参数,一个 eventName
(a string
) 和一个callBack
(a function
)。
这个eventName
应该是attributeInThePassedObject + "Changed"
; 因此,firstNameChanged
从基础对象中的firstName
属性派生。
该callBack
函数在调用时:
- 应传递与名称关联的类型的值
attributeInThePassedObject
;因此,由于firstName
类型为string
,事件的回调firstNameChanged
期望在调用时将 astring
传递给它。类似地,与关联的事件age
应该期望用number
参数调用 - 应该有
void
返回类型(为了演示的简单性)
因此, 的原始函数签名on()
可能是:on(eventName: string, callBack: (newValue: any) => void)
。但是,在前面的描述中,我们确定了我们希望在代码中记录的重要类型约束。模板文字类型让我们将这些约束带入我们的代码中。
1 | const person = makeWatchedObject({ |
请注意,on
监听事件"firstNameChanged"
,而不仅仅是"firstName"
. on()
如果我们要确保符合条件的事件名称集受监视对象中属性名称的联合约束,并在末尾添加“已更改”,我们的天真规范可能会变得更加健壮。虽然我们很乐意在 JavaScript ie 中进行这样的计算Object.keys(passedObject).map(x =>
${x}Changed)
,但类型系统中的模板文字提供了类似的字符串操作方法:
1 | type PropEventSource<Type> = { |
有了这个,我们可以构建一些在给定错误属性时出错的东西:
1 | const person = makeWatchedObject({ |
用模板文字推断
请注意,我们并未受益于原始传递对象中提供的所有信息。给定 a 的变化firstName
(即firstNameChanged
事件),我们应该期望回调将接收 type string
的参数。同样,更改为的回调age
应该接收一个number
参数。我们天真地使用any
to type thecallBack
的参数。同样,模板文字类型可以确保属性的数据类型与该属性的回调的第一个参数的类型相同。
使这成为可能的关键见解是:我们可以使用具有泛型的函数,这样:
- 第一个参数中使用的文字被捕获为文字类型
- 该文字类型可以被验证为在泛型中的有效属性的联合中
- 可以使用索引访问在泛型的结构中查找经过验证的属性的类型
- 然后可以应用此类型信息以确保回调函数的参数属于同一类型
1 | type PropEventSource<Type> = { |
这里我们制作成on
泛型方法。
当用户使用字符串调用时"firstNameChanged"
,TypeScript 将尝试为 推断正确的类型Key
。为此,它将匹配Key
之前的内容"Changed"
并推断字符串"firstName"
。一旦 TypeScript 弄清楚了这一点,该方法就可以获取原始对象的on
类型,在本例中就是这样。同样,当使用调用时,TypeScript 会找到属性的类型。firstName``string``"ageChanged"``age``number
推理可以以不同的方式组合,通常是解构字符串,并以不同的方式重建它们。
内部字符串操作类型
为了帮助进行字符串操作,TypeScript 包含一组可用于字符串操作的类型。这些类型内置于编译器中以提高性能,并且无法在TypeScript 附带的文件.d.ts
中找到。
Uppercase
将字符串中的每个字符转换为大写版本。
例子
1 | type Greeting = "Hello, world" |
Lowercase
将字符串中的每个字符转换为等效的小写字母。
例子
1 | type Greeting = "Hello, world" |
Capitalize
将字符串中的第一个字符转换为等效的大写字母。
例子
1 | type LowercaseGreeting = "hello, world"; |
Uncapitalize
将字符串中的第一个字符转换为等效的小写字母。
例子
1 | type UppercaseGreeting = "HELLO WORLD"; |
内在字符串操作类型的技术细节
从 TypeScript 4.1 开始,这些内部函数的代码直接使用 JavaScript 字符串运行时函数进行操作,并且无需了解区域设置。
1 | function applyStringMapping(symbol: Symbol, str: string) { |
类
类成员
属性、方法、readonly、、getters/setters、索引签名
getters/setters
请注意,一个没有额外逻辑的字段支持的 get/set 对在JavaScript中很少有用。如果你不需要在get/set 操作中添加额外的逻辑,暴露公共字段也是可以的。
TypeScript对访问器有一些特殊的推理规则:
- 如果存在 get ,但没有 set ,则该属性自动是只读的
- 如果没有指定 setter 参数的类型,它将从 getter 的返回类型中推断出来
- 访问器和设置器必须有相同的成员可见性
从TypeScript 4.3开始,可以有不同类型的访问器用于获取和设置
1 | class Thing { |
访问器和设置器必须有相同的成员可见性?
在 TypeScript 中,访问器和设置器必须有相同的成员可见性。也就是说,如果您为一个成员定义了访问器或设置器,那么这个成员的访问级别必须与访问器或设置器中的访问级别相同。
例如,如果您为一个类的私有成员定义了访问器或设置器,那么这个访问器或设置器的访问级别也必须是私有的。同样,如果您为一个类的受保护成员定义了访问器或设置器,那么这个访问器或设置器的访问级别也必须是受保护的。
以下是一个 TypeScript 类的例子,其中访问器和设置器具有相同的成员可见性:
1 | class MyClass { |
在这个例子中,MyClass
类定义了一个私有成员变量 _value
,并为它定义了一个访问器 get value()
和一个设置器 set value(newValue: number)
。由于 _value
是私有成员,因此访问器和设置器的访问级别也必须是私有的。
访问器和设置器的可见性要求确保了类的封装性和安全性,防止了对类的私有或受保护成员的直接访问。
构造函数(构造器)
在 TypeScript 中,类的构造函数可以接受参数,并且这些参数可以定义它们的类型。但是,类的构造函数不能有类型参数。
这是因为类型参数是在使用泛型时定义的,它们是在编译时确定的。而构造函数是在运行时调用的,因此在构造函数中定义类型参数是没有意义的。
如果您想在类中使用泛型,可以将泛型类型参数定义为类级别的类型参数,然后在类中的方法和属性中使用它们。例如:
1 | class MyGenericClass<T> { |
在这个例子中,MyGenericClass
类接受一个类型参数 T
,并在构造函数中接受一个类型为 T
的参数 value
。然后,getValue
方法返回类型为 T
的值。
您可以实例化该类并将不同类型的值传递给构造函数:
1 | const myStringClass = new MyGenericClass<string>('hello'); |
构造函数不能有返回类型注释
在 TypeScript 中,构造函数不能有显式的返回类型注释,因为构造函数的返回值是由类本身确定的,而不是由构造函数的实现决定的。
当您使用 new
操作符实例化一个类时,构造函数会返回该类的实例,而且该实例的类型就是该类本身。因此,您不需要在构造函数中指定返回类型。
以下是一个 TypeScript 类的例子:
1 | class MyClass { |
在这个例子中,MyClass
类的构造函数接受一个 message
参数,并将其保存在类的私有成员变量中。然后,类定义了一个 showMessage
方法,它输出 message
的值。
当我们使用 new
操作符创建一个 MyClass
实例时,不需要指定返回类型:
1 | const myInstance = new MyClass('Hello, world!'); |
因为构造函数的返回值是 MyClass
类的实例,它的类型已经由类本身决定了。
类继承
implements
- 继承接口,需要实现该接口的方法
- 类也可以实现多个接口,逗号隔开,例如:
class c implements A, B {
注意事项
implements 子句只是检查类是否可以被当作接口类型来对待。它根本不会改变类的类型或其方法。一个常见的错误来源是认为 implements 子句会改变类的类型–它不会!
1 | interface Checkable { |
在这个例子中,我们也许期望 s 的类型会受到 check 的 name: string 参数的影响。事实并非如此–实现子句并没有改变类主体的检查方式或其类型的推断。
同样地,实现一个带有可选属性的接口并不能创建该属性。
1 | interface A { |
extends
类可以从基类中扩展出来。派生类拥有其基类的所有属性和方法,也可以定义额外的成员。
重写方法
1 | class Base { |
派生类遵循其基类契约是很重要的。请记住,通过基类引用来引用派生类实例是非常常见的(而且总是合法的!)
1 | // 通过基类引用对派生实例进行取别名 |
如果 Derived 没有遵守Base的约定怎么办?
1 | class Derived extends Base { |
如果我们不顾错误编译这段代码,这个样本就会崩溃:
1 | const b: Base = new Derived(); |
初始化顺序
让我们看看一下这段代码:
1 | class Base { |
这里发生了什么?
按照JavaScript的定义,类初始化的顺序是:
- 基类的字段被初始化
- 基类构造函数运行
- 派生类的字段被初始化
- 派生类构造函数运行
这意味着基类构造函数在自己的构造函数中看到了自己的name值,因为派生类的字段初始化还没有运行。
1 | class Base { |
输出
1 | My name is base |
继承内置类型
注意:如果你不打算继承Array、Error、Map等内置类型,或者你的编译目标明确设置ES6/ES2015或以上,你可以跳过本节。
在ES2015中,返回对象的构造函数隐含地替代了 super(…) 的任何调用者的 this 的值。生成的构造函数代码有必要捕获 super(…) 的任何潜在返回值并将其替换为 this 。因此,子类化 Error 、 Array 等可能不再像预期那样工作。这是由于 Error 、 Array 等的构造函数使用ECMAScript 6的 new.target 来调整原型链;然而,在ECMAScript 5中调用构造函数时,没有办法确保 new.target 的值。其他的下级编译器一般默认有同样的限制。
instanceof 将在子类的实例和它们的实例之间被打破,所以 (new MsgError())instanceofMsgError 将返回 false
1 | class MsgError extends Error { |
你可以在任何 super(…) 调用后立即手动调整原型。
1 | class MsgError extends Error { |
成员可见性
public
所有可访问
protected
只对他们所声明的子类可见,子类中可访问,子类实例后不可访问。
派生类需要遵循它们的基类契约,但可以选择公开具有更多能力的基类的子类型。这包括将受保护的成员变成公开。
1 | class Base { |
private
派生类不可访问,子类实例后不可访问。
1 | class Base { |
1 | class Base { |
静态成员
类可以有静态成员。这些成员并不与类的特定实例相关联。它们可以通过类的构造函数对象本身来访问。
1 | class MyClass { |
静态成员也可以使用相同的 public 、 protected 和 private 可见性修饰符。
1 | class MyClass { |
静态成员也会被继承。
1 | class Base { |
特殊静态名称
一般来说,从函数原型覆盖属性是不安全的/不可能的。因为类本身就是可以用 new 调用的函数,所以某些静态名称不能使用。像 name 、 length 和 call 这样的函数属性,定义为静态成员是无效的。
1 | class S { |
为什么没有静态类
TypeScript(和JavaScript)没有像C#和Java那样有一个叫做静态类的结构。
这些结构体的存在,只是因为这些语言强制所有的数据和函数都在一个类里面;因为这个限制在TypeScript中不存在,所以不需要它们。一个只有一个实例的类,在JavaScript/TypeScript中通常只是表示为一个普通的对象。
例如,我们不需要TypeScript中的 “静态类 “语法,因为一个普通的对象(甚至是顶级函数)也可以完成
这个工作。
1 | // 不需要 "static" class |
类里的 static区块
静态块允许你写一串有自己作用域的语句,可以访问包含类中的私有字段。这意味着我们可以用写语句的所有能力来写初始化代码,不泄露变量,并能完全访问我们类的内部结构
1 | class Foo { |
类运行时中的this
TypeScript并没有改变JavaScript的运行时行为,而JavaScript的运行时行为偶尔很奇特。
1 | class MyClass { |
长话短说,默认情况下,函数内this的值取决于函数的调用方式。在这个例子中,因为函数是通过obj引用调用的,所以它的this值是obj而不是类实例。
箭头函数
如果你有一个经常会被调用的函数,失去了它的 this 上下文,那么使用一个箭头函数而不是方法定义是有意义的。
1 | class MyClass { |
这有一些权衡:
- this 值保证在运行时是正确的,即使是没有经过TypeScript检查的代码也是如此。
- 这将使用更多的内存,因为每个类实例将有它自己的副本,每个函数都是这样定义的。
- 你不能在派生类中使用 super.getName ,因为在原型链中没有入口可以获取基类方法。
this 参数
在方法或函数定义中,一个名为 this 的初始参数在TypeScript中具有特殊的意义。这些参数在编译过程
中会被删除。
1 | // 带有 "this" 参数的 TypeScript 输入 |
1 | // 编译后的JavaScript结果 |
TypeScript检查调用带有 this 参数的函数,是否在正确的上下文中进行。我们可以不使用箭头函数,而是在方法定义中添加一个 this 参数,以静态地确保方法被正确调用。
1 | class MyClass { |
这种方法做出了与箭头函数方法相反的取舍:
JavaScript调用者仍然可能在不知不觉中错误地使用类方法
每个类定义只有一个函数被分配,而不是每个类实例一个函数
基类方法定义仍然可以通过 super 调用。
this类型
在类中,一个叫做 this 的特殊类型动态地指向当前类的类型。
1 | class Box { |
输出结果
1 | Box ClearableBox { content: 'hello' } |
Box类set方法的this指向了ClearableBox类。
参数类型注释中使用this,如果你有一个派生类,它的sameAs方法现在只接受该同一派生类的其他实例了。
1 | class Box { |
基于类型守卫的this
常用的一个地方是允许对一个特定字段进行惰性验证。
1 | class Box<T> { |
参数属性
在构造函数中加入参数属性,可以简写了,不用在外面定义属性了。
1 | class Params { |
类表达式写法
1 | const someClass = class<Type> { |
抽象类和成员
抽象类不能被实例化,即不能用new,需要常见派生类来实现抽象成员。
1 | abstract class Base { |
抽象构造签名
有时候你想接受一些类的构造函数,产生一个从某些抽象类派生出来的类的实例。
可能想这样写:
1 | function greet(ctor: typeof Base) { |
Typescript正确地告诉你,你正试图实例化一个抽象类。毕竟,鉴于green的定义,写的这段代码是完全合法的,他最终会构造一个抽象类。
1 | // bad |
相反,你想写一个函数,接受具有结构化签名的东西:
1 | function greet(ctor: new () => Base) { |
现在TypeScript正确地告诉你哪些类的构造函数可以被调用: Derived 可以,因为它是具体的,但Base 不能。
1 | # 类型“typeof Base”的参数不能赋给类型“new () => Base”的参数。 |
类之间的关系
相同的两个类之间可以相互代替使用
1 | class Point1 { |
包含关系的两个类之间也可以使用,小的类作为类型。这句话的理解:即使没有明确的继承,类之间的子类型关系也是存在的。
1 | class Point1 { |
下面这种情况会特殊点,空的类没有成员,在一个结构化类型系统中,一个没有成员的类型通常是其他任何类型的超类。
1 | class Empty { |
模块
认识模块
主要考虑三个:
- 语法:我想用什么语法来导入和导出?
- 模块解析:模块名称(或路径)和磁盘上的文件之间是什么关系?
- 模块输出目标:我编译出来的js模块应该是什么样子的?
额外的导入语法
示例的目录结构
1 | +-- src |
export.ts
1 | export const pi = 3.14; |
index.ts 引入 export.ts
写法一
1 | import RNGen, { pi as π } from './export'; |
写法二
1 | import * as math from './export'; |
TS特定的ES模块语法
如果是扩展类型和常规结合使用,写法如下:
示例的目录结构
1 | +-- src |
export.ts
1 | export type Cat = { |
index.ts
如果只引入扩展类型
1 | import { Cat, Dog } from './export'; |
或者
1 | import type { Cat, Dog } from './export'; |
如果引入扩展类型和非扩展类型的其他东西,如函数
1 | import { createCatName, type Cat, type Dog } from './export'; |
注意,引入export.ts
时,是不带.ts
的,因为编译后的是js,而非ts文件。
声明合并
介绍
TypeScript 中的一些独特概念在类型级别描述了 JavaScript 对象的形状。TypeScript 特别独特的一个例子是“声明合并”的概念。理解这个概念将使您在使用现有 JavaScript 时更有优势。它还为更高级的抽象概念打开了大门。
就本文而言,“声明合并”是指编译器将两个单独的同名声明合并为一个定义。这个合并的定义具有两个原始声明的特征。可以合并任意数量的声明;它不仅限于两个声明。
基本概念
在 TypeScript 中,声明至少在三组中的一组中创建实体:命名空间、类型或值。创建命名空间的声明创建了一个命名空间,其中包含使用点分符号访问的名称。类型创建声明就是这样做的:它们创建一个类型,该类型对声明的形状可见并绑定到给定的名称。最后,创建值的声明创建在输出 JavaScript 中可见的值。
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
了解每个声明创建的内容将帮助您了解执行声明合并时合并的内容。
合并接口
最简单,也许是最常见的声明合并类型是接口合并。在最基本的层面上,合并将两个声明的成员机械地连接到一个同名的接口中。
1 | interface Box { |
接口的非函数成员应该是唯一的。如果它们不是唯一的,则它们必须属于同一类型。如果接口都声明了同名但类型不同的非函数成员,编译器将发出错误。
对于函数成员,每个同名的函数成员都被视为描述同一函数的重载。同样值得注意的是,在接口A
与后来的接口A
合并的情况下,第二个接口将比第一个接口具有更高的优先级。
也就是说,在示例中:
1 | interface Cloner { |
这三个接口将合并以创建一个声明,如下所示:
1 | interface Cloner { |
请注意,每个组的元素都保持相同的顺序,但组本身是合并的,后来的重载组先排序。
此规则的一个例外是专用签名。如果签名的参数类型是单个字符串文字类型(例如,不是字符串文字的并集),那么它将冒泡到其合并的重载列表的顶部。
并集也称联合
例如,以下接口将合并在一起:
1 | interface Document { |
生成的合并声明Document
如下:
1 | interface Document { |
合并命名空间
与接口类似,同名的命名空间也会合并它们的成员。由于命名空间同时创建命名空间和值,我们需要了解两者如何合并。
为了合并命名空间,每个命名空间中声明的导出接口的类型定义本身被合并,形成一个内部具有合并接口定义的命名空间。
要合并名称空间值,在每个声明站点,如果已存在具有给定名称的名称空间,则通过采用现有名称空间并将第二个名称空间的导出成员添加到第一个名称空间来进一步扩展它。
本例中的声明合并Animals
:
1 | namespace Animals { |
相当于:
1 | namespace Animals { |
这种名称空间合并模型是一个有用的起点,但我们还需要了解非导出成员会发生什么。非导出成员仅在原始(未合并的)命名空间中可见。这意味着合并后,来自其他声明的合并成员看不到非导出成员。
我们可以在这个例子中更清楚地看到这一点:
1 | namespace Animal { |
因为haveMuscles
未导出,所以只有animalsHaveMuscles
共享相同未合并命名空间的函数才能看到该符号。该doAnimalsHaveMuscles
函数,即使它是合并Animal
命名空间的一部分,也看不到这个未导出的成员。
将命名空间与类、函数和枚举合并
命名空间足够灵活,可以与其他类型的声明合并。为此,名称空间声明必须跟在它将合并的声明之后。生成的声明具有两种声明类型的属性。TypeScript 使用此功能对 JavaScript 和其他编程语言中的某些模式进行建模。
将命名空间与类合并
这为用户提供了一种描述内部类的方法。
1 | class Album { |
合并成员的可见性规则与合并命名空间部分中描述的相同,因此我们必须导出类AlbumLabel
以供合并类查看。最终结果是在另一个类内部管理一个类。您还可以使用名称空间向现有类添加更多静态成员。
除了内部类模式之外,您可能还熟悉创建函数然后通过向函数添加属性来进一步扩展函数的 JavaScript 实践。TypeScript 使用声明合并以类型安全的方式构建这样的定义。
1 | function buildLabel(name: string): string { |
同样,命名空间可用于扩展具有静态成员的枚举:
1 | enum Color { |
不允许合并
并非所有合并都在 TypeScript 中被允许。目前,类不能与其他类或变量合并。有关模拟类合并的信息,请参阅TypeScript 中的混合部分。
模组扩充
尽管 JavaScript 模块不支持合并,但您可以通过导入然后更新现有对象来修补它们。让我们看一个玩具 Observable 示例:
1 | // observable.ts |
这在 TypeScript 中也能正常工作,但编译器不知道Observable.prototype.map
. 您可以使用模块扩充将其告知编译器:
1 | // observable.ts |
import
模块名称的解析方式与/中的模块说明符相同export
。有关详细信息,请参阅模块。然后合并扩充中的声明,就好像它们是在与原始文件相同的文件中声明的一样。
但是,请记住两个限制:
- 您不能在扩充中声明新的顶级声明——只是对现有声明的补丁。
- 默认导出也不能被扩充,只能被命名为导出(因为您需要通过其导出名称来扩充导出,并且
default
是一个保留字 - 有关详细信息,请参见#14080)
全局增强
您还可以从模块内部向全局范围添加声明:
1 | // observable.ts |
全局扩充与模块扩充具有相同的行为和限制。
issues
创建一个对象会做哪三件事
js垃圾回收机制触发时机
ts编译为es5,class编译成了立即执行函数?
避免变量名别污染
静态方法中只能调用静态的方法或属性?
是的,跟非静态方法或属性是独立的,反之也是。
一个静态方法改变了某个静态属性,其他静态方法或类外部任何地方访问这个属性都会发生改变?
是。这个非静态方法,new object有区别。
静态属性或方法分配内存空间的时间早于对象空间的分配?
是。静态属性或方法分配内存空间会一直在,直到程序执行结束才被释放。