最后
面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
生态系统一致性,OK!
3. 一致的 tsconfig 设置是值得的
tsconfig 提供的灵活性主要在于,它使你可以让 TypeScript 适应你的运行时平台。在所有项目都以同一个常绿运行时为目标的环境中,事实证明对每个项目进行单独配置是风险很大的。
生态系统一致性?
因此,我们让工具链负责在构建时使用“理想”设置生成 tsconfig。例如,默认情况下启用“strict”模式以增加类型安全性。强制执行“isolatedModules”,以通过每次操作一个文件的简单编译器快速编译我们的代码。
将 tsconfig 视为生成的文件(而非源文件)的另一个好处是,它允许高层工具链负责定义“references”和“paths”之类的选项,从而将多项目“工作区”灵活地链接在一起。
这里出了些问题,因为少数项目希望能够自定义,例如切换到较宽松的模式以减轻迁移负担。
一开始我们试图满足这些要求,并提供了一些选项。后来我们发现,当使用一组选项构建的声明文件被使用不同选项的程序包占用时,就会导致程序包间冲突。下面是一个例子。
可以创建一个由“strictNullChecks”值定向的条件类型。
type A = unknown extends {} ? string : number;
如果启用了“strictNullChecks”,则 A 为一个 number。如果禁用了“strictNullChecks”,则 A 为一个 string。如果导出此类型的包未使用与导入它的包相同的严格性设置,这段代码就会中断。以上是我们面临的现实问题的简化示例。结果,我们选择弃用严格性模式的灵活性,换取对所有项目都有一致的配置。
生态系统一致性,OK!
4. 如何指定依赖项的位置很重要
我们需要明确声明 TypeScript 依赖项的位置。这是因为我们的 ES 模块系统不依赖“通过遍历一系列名为 node_modules 的目录来查找依赖项”的 Node 文件系统约定。
我们需要能够声明 bare-specifier(裸指定符,例如“lodash”)到磁盘上目录位置(“c:\dependencies\lodash”)的映射。这很像是试图解决 Web 类似问题的 import maps。首先,我们尝试在 tsconfig 中使用“paths”选项。
// tsconfig.json
“paths”: {
“lodash”: [ “…/…/dependencies/lodash” ]
}
这几乎适用于所有用例。但我们发现它降低了生成的声明文件的质量。TypeScript 编译器必须将合成(synthetic)的 import 语句注入声明文件中,以允许使用复合类型——其中的类型可以取决于其他模块的类型。当合成的 import 引用依赖项中的类型时,我们发现“paths”方法注入了相对路径(import(“…/…/dependencies/lodash”)),而不是保留裸指定符(import “lodash”)。对于我们的系统来说,外部包类型的相对位置是可能会更改的实现细节,因此这是不可接受的。
生态系统一致性?
我们找到的解决方案是使用 Ambient 模块:
// ambient-modules.d.ts
declare module “lodash” {
export * from “…/…/dependencies/lodash”;
export default from “…/…/dependencies/lodash”;
}
Ambient 模块是特殊的。TypeScript 的声明发射保留对它们的引用,而不是将其转换为相对路径。生态系统一致性,OK!
5. 避免重复类型很重要
应用的性能是关键指标,因此我们试着尽量减少应用在运行时加载的 JS 数量。我们的平台确保在运行时仅使用一个版本的软件包。移除版本的重复数据意味着给定的包不能“冻结”或“固定”其依赖项。因此,这意味着软件包必须时刻保持兼容性。
我们希望为类型提供相同的“完全唯一(exactly-one)”保证,以确保对于给定的项目编译,类型检查仅考虑软件包依赖项的一个版本。除了提高编译时效率外,这里的动机还在于确保类型检查的世界更好地反映运行时世界。我们特别想避免陈旧(staleness)问题和“nominal 地狱”,在这些情况下可能会通过“钻石模式”导入两个不兼容的 nominal 类型版本。随着生态系统采用的 nominal 类型日益增多,这种危害也可能随之加剧。
可扩展性?生态系统一致性?
我们编写了一个确定性解析器,其根据所构建软件包的声明版本,确保为每个依赖项只选择一个版本。
可扩展性,OK!生态系统一致性,OK
这意味着类型依赖图是动态组合的——它不会冻结。尽管这种非固定的依赖方法可以带来很多好处并避免了某些危害,但我们后来了解到,由于 TypeScript 编译器中的一些行为细节,它可能会带来新的危害。请参阅第 9 部分以了解更多信息。
这些折衷和选择不是只适用于我们自己的平台。它们同样适用于发布到 DefinitelyTyped/npm 的任何人,并取决于 package.json "dependencies"中表示的所有包版本约束的累加效果。
6. 应避免隐式类型依赖
在 TypeScript 中引入全局类型很容易。依赖全局类型甚至更容易。如果不加以检查,那么在距离遥远的包之间可能出现隐藏的耦合。TypeScript 手册称其为“有点危险”。
**可扩展性?生态系统一致性?
**
// A declaration that injects global types
declare global {
interface String {
fancyFormat(opts?: StringFormatOptions): string;
}
}
// Somewhere in a file far, far away…
String.fancyFormat(); // no error!
这里的解决方案大家都熟悉:相对于全局状态,优先使用显式依赖。TypeScript 长期以来一直为 ECMAScript 的 import 和 export 语句提供支持,从而实现了这一目标。
因此,剩下的唯一需求是防止意外创建全局类型。所幸我们可以静态检测 TypeScript 允许引入全局类型的所有情况。于是我们更新了工具链,以检测并报错这些情况。也就是说我们可以放心地确认一个事实,即导入一个包的类型是无副作用的操作。
可扩展性,OK!生态系统一致性,OK!
7. 声明文件具有三种导出模式
并非所有的声明文件都相等。声明文件根据其内容,会以三种模式之一运行;特别是 import 和 export 关键字的用法会有不同。
-
global——不使用 import 或 export 的声明文件将被视为 global。顶级声明是全局导出的。
-
module——具有至少一个 export 声明的声明文件将被视为模块。只有 export 声明会被导出,不会定义任何 global。
-
隐式 export——没有 export 声明,但使用 import 的声明文件将触发已定义但尚未说明的行为。也就是将顶级声明视为命名的 export 声明,并且不会定义 global。
我们不使用第一种模式。我们的工具链会避免使用全局声明文件(请参见上一节)。这意味着所有声明文件都使用 ES 模块语法。
可扩展性,OK!生态系统一致性,OK!标准对齐,OK!
也许会令人惊讶的是,我们发现看起来有点诡异的第三种模式很有用。通过在 ambient 声明文件的顶部只添加单行 self-import,可以防止它们污染全局名称空间:import {} from “./”;。这种单行代码简化了将第三方声明(例如 lib.dom.d.ts)转换为模块化的操作,并且避免了维护更复杂的 fork 的麻烦。
TypeScript 团队似乎并不喜欢第三种模式,因此请尽可能避免使用第三种模式。
8. 包的封装可能出问题
如前所述(第 5 节),我们使用未固定的依赖项意味着:对于我们的包来说,不仅要保留运行时兼容性,还要时刻保持类型兼容性,这一点很重要。这是一个挑战,因此要确保兼容性能保持下去,我们必须深度了解哪些类型被公开,并且必须以这种方式加以约束。第一步是明确区分公共模块与私有模块。
Node 最近以 package.json “exports” 字段的形式获得了这种能力。它通过显式列出可从包外部访问的文件来定义封装边界。
如今,TypeScript 尚不了解 package exports,因此不理解依赖项中的哪些文件被视为公共或私有的概念。在声明生成期间,当 TypeScript 在发射的.d.ts 文件中合成 import 语句以传递类型时,这就成为了一个问题。我们的.d.ts 文件引用其他包中的私有文件是不可接受的。下面是一个出错的例子。
// index.ts
import boxMaker from “another-package”
export const box = boxMaker();
以上源可能导致 tsc 发出以下不良声明。
// index.d.ts
export const box : import(“another-package/private”).Box
这就不对了,因为“another-package/private”不属于这个包的兼容性保证,因此可以在没有 SemVer 重大 bump 的情况下进行移动或重命名。如今,TypeScript 无法知道它生成的是一个脆弱的导入。
生态系统一致性?
我们使用两个步骤来缓解这一问题:
1、我们的工具链会向 TypeScript 解析器通知指向依赖项的,有意公开的裸指示符路径(例如“lodash/public1”“lodash/public2”)。我们在 TypeScript 文件流入编译器之前,静默地将 type-only 的导入语句添加到 TypeScript 文件的底部,从而确保 TypeScript 了解全部合法依赖项的入口点。
// user’s source code
// injected by toolchain to assist declaration emit
import type * as __fake_name_1 from “lodash/public1”;
import type * as __fake_name_2 from “lodash/public2”;
在生成对推断的传递类型的引用时,TypeScript 的声明发射会优先使用这些现有的名称空间标识符,而不是合成对私有文件的导入。
2、如果 TypeScript 对我们知道是私有的依赖项中的文件生成路径,则工具链会报错。当 TypeScript 意识到它正在生成一个依赖项的潜在危险路径时,也会报错,这两种错误很像。
error TS2742: The inferred type of ‘…’ cannot be named without a reference to ‘…’.
This is likely not portable. A type annotation is necessary.
这会通过显式注解导出来通知用户解决问题。或者在某些情况下,他们需要直接从公共包入口点导出内部类型来更新依赖项,以公开内部类型。
生态系统一致性,OK!
我们期待 TypeScript 获得对入口点的一等支持,这样就用不着这种解决方法了。
9. 生成的声明可以内联依赖项中的类型
程序包需要导出.d.ts 声明,以便用户可以消费它们。我们选择使用 TypeScript 的 declaration 选项从原始.ts 文件生成.d.ts 文件。尽管我们可以与常规代码一起手写和维护.d.ts 兄弟文件,但这种方法不太可取,因为保持它们同步意味着一种危险。
在大多数情况下,TypeScript 的声明发射很好用。我们发现的一个问题是,有时 TypeScript 会将类型从依赖项内联到生成的类型中(#37151)。这意味着类型定义将被重定位,并可能被复制,而不是通过导入语句进行引用。使用结构化类型时,编译器不必强制类型是从一个定义站点引用的——这些类型可以复制。
我们还发现了一些极端情况,其中这种复制让声明文件从 7KB 膨胀到了 700KB,冗余代码实在太多了。
可扩展性?
包内类型的内联不是生态系统问题,因为它在外部不可见。当跨包边界内联类型时就出问题了,因为它将这两个特定版本耦合在一起。在我们的非固定包系统中,每个包都可以独立进化。这意味着存在类型不兼容的风险,尤其是类型陈旧的风险。
生态系统一致性?
-
通过实验,我们发现了防止内联类型声明的一些可选方法,例如:
-
首选 interface 而不是 type(接口不内联)
-
如果未导出声明所需的 interface,则 tsc 将拒绝内联该类型并生成明显错误(例如,TS4023: Exported variable has or is using name from external module but cannot be named.)
-
如果未导出生成的声明所需的 type,则 tsc 将静默内联该类型
-
Nicholas Jamieson 写了一篇关于优先使用 interface 而非 type 的文章,包括了一条 ESlint 规则
-
使类型 nominal(带有私有成员的 enum 和 class 之类的 nominal 类型不被内联)
-
将类型注解添加到导出
-
- 没有注解,就会内联
-
用显式类型注解,我们可以强制引用行为
可扩展性,OK;生态系统一致性,OK
这种内联行为似乎没有被严格指定。这是声明文件构造方式的副作用。因此,上述方法将来可能无法使用。我们希望这是可以在 TypeScript 中形式化的内容。在此之前,我们将依靠用户培训来缓解这种风险。
10. 生成的声明可以包含非必要依赖项
TypeScript 声明文件的消费者通常只关心包的公共类型 API。TypeScript 声明发射会为项目中的每个 TypeScript 文件恰好生成一个声明文件。这些内容中某些可能与用户无关,并且可能会暴露私有的实现细节。这种行为对于 TypeScript 的新手来说可能很难想象,他们希望类型是公共 API 的表示,就像在“Definitely Typed”上找到的手写类型一样。
其中一个示例是:生成的声明包括仅用于内部测试的函数类型。
可扩展性?
由于我们的包系统知道所有公共包的入口点,因此我们的工具链可以爬取可达类型的图,以识别出不需要公开的所有类型。这就是死类型消除(DTE),或更确切地说是摇树。我们编写了一个工具来执行这一操作——它只从声明文件中消除代码,这样任务最轻松。它不会重写或重定位代码——毕竟它不是打包器。这意味着发布的声明是 TypeScript 生成声明的一个不变子集。
减少发布类型的数量有几个优点:
-
它减少了与其他软件包的耦合(某些软件包不会从其依赖项中重新导出类型);
-
它防止了完全私有的类型泄漏,从而改善了封装;
-
它减少了需要用户下载和解压缩的已发布声明文件的数量和大小;
-
它减少了 TypeScript 编译器在类型检查时必须解析的代码量。
这种“摇树”会带来显著的效果。我们发现,有些包可以删除 90%以上的文件和 90%以上的类型代码行。
可扩展性,OK!
有些选项效果很不错
我们在某些 tsconfig 选项的语义中发现了一些惊喜。
tsconfig 中的 baseUrl
在 TypeScript 4.0 中,如果要使用项目引用或“paths”,则还需要指定一个 baseUrl。这样做的副作用是导致所有裸指定符的导入都相对于项目的根目录进行解析。
// package-a/main.ts
import “sibling” // Will auto-complete and type-check if package-a/sibling.js
exists
这里的危险在于,如果要引入任何形式的“paths”,则会带来额外的含义,使 import "sibling"被 TypeScript 意外地解析为从源目录内部导入的/sibling.js。
标准对齐?
为解决问题,我们使用了一个 baseUrl。使用 null 字符可以防止意外的自动完成。我们不建议你在家尝试。
我们在 TypeScript 问题跟踪器上报告了这个 issue,很高兴看到 Andrew 在 TypeScript 4.1 中解决了它,我们可以告别 null 字符了!
标准对齐,OK!
JSON 模块暗示合成默认导入
文末
逆水行舟不进则退,所以大家要有危机意识。
同样是干到35岁,普通人写业务代码划水,榜样们深度学习拓宽视野晋升管理。
这也是为什么大家都说35岁是程序员的门槛,很多人迈不过去,其实各行各业都是这样都会有个坎,公司永远都缺的高级人才,只用这样才能在大风大浪过后,依然闪耀不被公司淘汰不被社会淘汰。
为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了《前端工程师核心知识笔记》电子稿文件。
内容包括html,css,JavaScript,ES6,计算机网络,浏览器,工程化,模块化,Node.js,框架,数据结构,性能优化,项目等等。
269页《前端大厂面试宝典》
包含了腾讯、字节跳动、小米、阿里、滴滴、美团、58、拼多多、360、新浪、搜狐等一线互联网公司面试被问到的题目,涵盖了初中级前端技术点。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
前端面试题汇总
只用这样才能在大风大浪过后,依然闪耀不被公司淘汰不被社会淘汰。
为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了《前端工程师核心知识笔记》电子稿文件。
内容包括html,css,JavaScript,ES6,计算机网络,浏览器,工程化,模块化,Node.js,框架,数据结构,性能优化,项目等等。
269页《前端大厂面试宝典》
包含了腾讯、字节跳动、小米、阿里、滴滴、美团、58、拼多多、360、新浪、搜狐等一线互联网公司面试被问到的题目,涵盖了初中级前端技术点。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
前端面试题汇总