TS+ 案例
2023 年注意事项:
TS+ 是 TS 的一个实验性分支,请勿在生产中使用它。
你好!
那么,这篇文章要讨论什么呢?
好吧,在 Effect 社区中,我们在过去 6 个月里一直在努力开发一些新东西,现在我很自豪地第一次详细介绍它。
目录
为什么选择 TS+?
让我们先来回顾一下背景故事。过去四年(是的,已经过去那么久了……),我们一直在努力开发Effect。
Effect 最初是一个小型项目,其灵感来源于 ZIO 开发人员在 Scala 中提出的想法。然而,它逐渐发展成为一个完整的 ZIO 核心模块移植版本,并拥有一套非常丰富的扩展模块,弥补了 Scala 和 TypeScript 中数据结构之间的差距。
在所有这些工作中,我们逐渐爱上了 TypeScript 语言,从第一天起我们就对我们可以做多少事情感到非常惊讶,而且从开发人员体验的角度来看,使用该语言是多么安全和愉快。
然而,我们也遇到了相当多的限制——实际上并不是对我们用该语言能做什么的限制,而是我们可以用该语言有效地做什么的限制,无论是在冗长程度方面,还是在生成的代码的可树摇动性方面。
让我们从一个例子开始:
Fluent API 的开发
TypeScript 中的流畅 API 可能看起来像这样:
export class IO<A> {
static succeed = <A>(a: A) => new IO(() => a);
static succeedWith = <A>(a: () => A) => new IO(a);
private constructor(readonly io: () => A) {}
map<A, B>(this: IO<A>, f: (a: A) => B): IO<B> {
return new IO(() => f(this.io()));
}
flatMap<A, B>(this: IO<A>, f: (a: A) => IO<B>): IO<B> {
return new IO(() => f(this.io()).io());
}
zip<A, B>(this: IO<A>, b: IO<B>): IO<[A, B]> {
return new IO(() => [this.io(), b.io()]);
}
run<A>(this: IO<A>) {
return this.io();
}
}
用法如下:
export const program = IO.succeed(0)
.map((n) => n + 1)
.flatMap((r) =>
IO.succeedWith(() => {
console.log(`result: ${r}`);
})
);
program.run();
在这种方法中,我们有一个数据类型IO<A>
和一个“伴随对象” IO
。这种方法适合于特定的代码组织:
- 构造函数像
succeed
和succeedWith
一样被放置在“伴随对象”中作为静态成员 map
诸如、flatMap
和 之类的方法zip
作为类型的成员放置在类实例中IO<A>
这种方法在面向对象语言中非常常见,其优势在于易于使用。事实上,对于此类模块的用户来说,编辑器体验非常棒——如果你想构造一个 ,你IO
只需输入IO.
,即可获得关于如何构造 的完整建议列表IO
。此外,当你想对 类型的值执行某些操作时IO<A>
,你只需输入value.
,然后会再次提示你一个漂亮的选项列表。
这种流畅的 API 类型是“一个梦想”,但也存在一些缺点,导致 FP 社区的大部分人都远离它。
其局限性令人震惊:
- 所有方法都必须在类内部(或 ADT 的公共抽象类内部)
- 一旦定义了一个类,从外部添加方法就需要模块扩充和对象原型的不安全变异
- 最糟糕的是,从摇树角度来看,所有这些都无法优化,所以最终你会得到一个巨大的包,在某个时候变得难以承受
FP 社区当前采用的“解决方案”是使用可管道 API。
Pipeable API 的开发
在可管道 API 中,我们将前面的示例重写为:
// file: IO.ts
export class IO<A> {
constructor(readonly io: () => A) {}
}
export const succeed = <A>(a: A) => new IO(() => a);
export const succeedWith = <A>(a: () => A) => new IO(a);
export function map<A, B>(f: (a: A) => B): (self: IO<A>) => IO<B> {
return (self: IO<A>) => new IO(() => f(self.io()));
}
export function flatMap<A, B>(f: (a: A) => IO<B>): (self: IO<A>) => IO<B> {
return (self) => new IO(() => f(self.io()).io());
}
export function zip<A, B>(b: IO<B>): (self: IO<A>) => IO<[A, B]> {
return (self) => new IO(() => [self.io(), b.io()]);
}
export function run<A>(self: IO<A>) {
return self.io();
}
// file: Function.ts
// omitted definition of pipe due to its length
// For the curious, take a look at https://github.com/Effect-TS/core/blob/master/packages/system/src/Function/pipe.ts
// file: Program.ts
import * as IO from "./IO";
import { pipe } from "./Function";
export const program = pipe(
IO.succeed(0),
IO.map((n) => n + 1),
IO.flatMap((r) =>
IO.succeedWith(() => {
console.log(`result: ${r}`);
})
)
);
IO.run(program);
我们本质上所做的就是提取类之外的所有构造函数和方法,并将参数移至this
函数将在每次函数调用中携带的普通柯里化函数参数pipe
。
最终的 API 在视觉上仍然相当不错,但它确实存在一些缺点。
首先,自动导入与命名空间导入配合得不太好(最近 Unsplash 的团队一直在为此开发语言服务插件)。即使你让自动导入正常工作,最终也会产生大量的导入。
此外,我们失去了 的意义和分类IO
——也就是说,我们不再拥有 数据类型IO<A>
和“伴生对象” IO
。现在,我们只拥有 数据类型IO<A>
和一个函数模块,该模块既包含构造函数(例如IO.succeed
),也包含可管道化的函数(也称为方面),例如IO.flatMap
。因此,在编程时,用户需要确切地知道要导入哪个模块,哪些函数是该数据类型的构造函数,哪些是该数据类型的方法,以及如何确切地使用它们。
最后,根据向其他开发人员教授这些概念的经验,人们有时在阅读可管道函数签名时遇到问题似乎很常见。
TS+ 简介
TS+ 是一种新语言,作为 TypeScript 的超集而开发,并作为原始 TypeScript 编译器的一个分支进行维护,每天都会重新制定。
为了保证对 TypeScript 生态系统的全面支持,我们限制了可扩展性。TS+ 会生成可供普通 TypeScript 使用的标准声明文件,并使用普通定义文件。TS+ 也不会添加任何新语法。
TS+ 与 TypeScript 的不同之处在于,我们不会限制自己使用类型信息来生成代码。相反,我们尽可能地利用类型信息来生成高度优化的代码,并提升开发者体验。
这些决策伴随着一系列的权衡。TS+ 只能使用 TS+ 编译器( 的一个分支tsc
)进行编译,而不能使用像 babel 这样“简单地”删除类型的工具进行编译。
我们建议使用一个编译管道,其中tsc
发出 ES2022 模块,然后使用另一个工具(例如esbuild
// swc
)babel
从那里接管,利用项目引用的强大基础架构来优化编译。
安装 TS+
要安装 TS+,您必须首先将其添加为开发依赖项:
yarn add -D @tsplus/installer
# or
npm i -D @tsplus/installer
然后,你可以将postinstall
脚本添加到你的package.json
。例如:
{
"scripts": {
"postinstall": "tsplus-install"
}
}
node_modules
这将用 TS+ 版本替换您安装的 TypeScript 。
注意:此安装过程是临时的,直到确定更好的安装机制。
安装后,您还需要确保您的 IDE 使用工作区中的 TypeScript 语言服务而不是默认服务。
如果您想开始使用预先配置的存储库,您可以在 GitPod 中打开https://github.com/ts-plus/starter-lib 。
使用 TS+(流利方法)
使用我们的初始示例,我们将首先从主IO
类中提取所有方法:
/**
* @tsplus type article.IO
*/
export class IO<A> {
static succeed = <A>(a: A) => new IO(() => a);
static succeedWith = <A>(a: () => A) => new IO(a);
constructor(readonly io: () => A) {}
}
/**
* @tsplus fluent article.IO map
*/
export function map<A, B>(self: IO<A>, f: (a: A) => B): IO<B> {
return new IO(() => f(self.io()));
}
/**
* @tsplus fluent article.IO flatMap
*/
export function flatMap<A, B>(self: IO<A>, f: (a: A) => IO<B>): IO<B> {
return new IO(() => f(self.io()).io());
}
/**
* @tsplus fluent article.IO zip
*/
export function zip<A, B>(self: IO<A>, b: IO<B>): IO<[A, B]> {
return new IO(() => [self.io(), b.io()]);
}
/**
* @tsplus fluent article.IO run
*/
export function run<A>(self: IO<A>) {
return self.io();
}
您可能注意到,在上面的示例中,我们为IO
类添加了一个 JSDoc 注释。此注释IO
使用 的“类型标签”来标识 TS+ 中的类型article.IO
。使用 TS+ 定义的每种类型的“类型标签”都应该是全局唯一的。使用包名作为类型标签的前缀很常见,因为非前缀标签应该保留给TS+ 标准库使用。
现在我们已经将类型标签添加到IO
类中,我们可以开始将类内部的每个方法提取到一个函数中,然后重命名this
为其他名称(在本例中我们选择使用self
)。
我们进一步在每个函数上方添加了 JSDoc 注解,用于将该函数与 TS+ 中的某个类型关联。例如,为了将flatMap
函数与IO
数据类型关联,我们flatMap
用 进行注解@tsplus fluent article.IO flatMap
。这实际上告诉编译器“将此流式方法放入任何与标签 article.IO 匹配的类型中,并将其命名为 flatMap”。
完成这些之后,我们就可以IO
像以前一样使用了:
export const program = IO.succeed(0)
.map((n) => n + 1)
.flatMap((r) =>
IO.succeedWith(() => {
console.log(`result: ${r}`);
})
);
program.run();
但我们已经可以从 IDE 中注意到以下内容:
我们从 IDE 快速文档中看到,这是一个“流畅”的扩展,而不是经典的“方法”。
流畅扩展就是这样,它们被声明为普通函数,甚至可以像 一样直接调用map(a, f)
。它们可以在任何你想要的地方声明,方法的路径将在编译期间解析。
为了支持那些喜欢管道 API 的用户,我们还提供了一个宏,用于从数据优先的函数派生出管道函数。其使用方法如下:
export function map<A, B>(self: IO<A>, f: (a: A) => B): IO<B> {
return new IO(() => f(self.io()));
}
const mapPipeable = Pipeable(map)
注意:这是一个编译器扩展,它将以正确的方式处理签名的泛型,它不使用条件类型。
此外,可以使用管道函数直接定义流畅的方法,例如:
/**
* @tsplus pipeable article.IO flatMap
*/
export function flatMap<A, B>(f: (a: A) => IO<B>): (self: IO<A>) => IO<B> {
return self => new IO(() => f(self.io()).io());
}
但这只解决了一半的问题。我们设法从数据类型中提取了方法,但构造函数仍然是“伴生对象”的静态成员(不可扩展且不可进行 tree-shaking)。
接下来我们来解决这个问题:
/**
* @tsplus type article.IO
* @tsplus companion article.IO/Ops
*/
export class IO<A> {
constructor(readonly io: () => A) {}
}
/**
* @tsplus static article.IO/Ops succeed
*/
export const succeed = <A>(a: A) => new IO(() => a);
/**
* @tsplus static article.IO/Ops succeedWith
*/
export const succeedWith = <A>(a: () => A) => new IO(a);
IO
这里,我们通过在IO
类上添加 注释来标识数据类型的“伴随”对象@tsplus companion article.IO/Ops
。然后,为了从类中提取构造函数,我们通常@tsplus static article.IO/Ops succeed
会说“将此值作为静态成员,添加到任何标记为 article.IO/Ops 的类型中”。
注意:类型标签和使用伴随定义的标签之间没有区别,但是一个类有两种类型:实例类型和构造函数类型,因此我们需要一种方法来区分哪个标签链接到什么。
对于那些不想使用类的人来说,另一种模式如下:
/**
* @tsplus type article.IO
*/
export interface IO<A> {
readonly io: () => A;
}
/**
* @tsplus type article.IO/Ops
*/
export interface IOOps {
<A>(io: () => A): IO<A>;
}
export const IO: IOOps = io => ({ io });
/**
* @tsplus static article.IO/Ops succeed
*/
export const succeed = <A>(a: A) => IO(() => a);
/**
* @tsplus static article.IO/Ops succeedWith
*/
export const succeedWith = <A>(a: () => A) => IO(a);
/**
* @tsplus fluent article.IO map
*/
export function map<A, B>(self: IO<A>, f: (a: A) => B): IO<B> {
return IO(() => f(self.io()));
}
/**
* @tsplus pipeable article.IO flatMap
*/
export function flatMap<A, B>(f: (a: A) => IO<B>): (self: IO<A>) => IO<B> {
return self => IO(() => f(self.io()).io());
}
/**
* @tsplus fluent article.IO zip
*/
export function zip<A, B>(self: IO<A>, b: IO<B>): IO<[A, B]> {
return IO(() => [self.io(), b.io()]);
}
/**
* @tsplus fluent article.IO run
*/
export function run<A>(self: IO<A>) {
return self.io();
}
//
// Usage
//
export const program = IO.succeed(0)
.map((n) => n + 1)
.flatMap((r) =>
IO.succeedWith(() => {
console.log(`result: ${r}`);
})
);
program.run();
我们通常做的另一件事是将相关类型放入IO
名为如下ExtractResult
的命名空间中IO
:
/**
* @tsplus type article.IO
*/
export interface IO<A> {
readonly io: () => A;
}
export declare namespace IO {
export type ExtractResult<I extends IO<any>> = [I] extends [IO<infer A>] ? A : never;
}
/**
* @tsplus type article.IO/Ops
*/
export interface IOOps {
<A>(io: () => A): IO<A>;
}
export const IO: IOOps = io => ({ io });
基本上给出IO
3个含义:
- 作为接口/类型(即方法)
- 作为术语/值(即构造函数)
- 作为命名空间(即相关类型)
所有这些结合在一起,提供了一种可扩展的开发方式,让我们能够轻松开发出可用且易于发现的 API,这些 API 完全可进行 tree-shaking 和优化。事实上,我们在这里编写的程序是一个使用了IO
我们定义的模块的有效程序。
使用 TS+(通话扩展)
在某些情况下,我们想向非函数添加调用签名,例如,我们可以重构上述构造函数,如下所示:
/**
* @tsplus type article.IO/Ops
*/
export interface IOOps {}
export const IO: IOOps = {};
/**
* @tsplus static article.IO/Ops __call
*/
export function make<A>(io: () => A): IO<A> {
return { io };
}
这允许我们使用我们定义的表达式来构造数据类型的值__call
。例如,我们可以IO<string>
使用上述数据类型__call
的表达式来创建一个IO
:
const io: IO<string> = IO(() => "Hello, World!")
名称__call
是一个特殊名称,其含义是“将函数用作类型的调用签名”。在上面的示例中,TS+ 会将数据类型__call
的表达式解析IO
为make
我们为其定义的函数IO
。
在某些极端情况下,您可能想要访问“this”,为此您可以使用__call
表达式的“流畅”变体而不是“静态”变体。
/**
* @tsplus fluent article.IO/Ops __call
*/
export function make<A>(self: IOOps, io: () => A): IO<A> {
return { io };
}
您可能认为我们迄今为止描述的一系列功能已经足够吸引人了……哦,但我们才刚刚开始。
使用 TS+(二元运算符)
JS 中有很多二元运算符 - 遗憾的是没有办法扩展它们(并且这样做的提议在摇树方面效率低下,有限,并且可能永远不会实现)...是的,我们也可以这样做:)
看看IO
我们上面定义的类型,zip
我们定义的组合器IO
看起来很像二元运算符。考虑到IO
在纯 JavaScript/TypeScript 中,在两种类型之间使用“+”符号没有任何意义,我们可以在 TS+ 中覆盖它:
/**
* @tsplus fluent article.IO zip
* @tsplus operator article.IO +
*/
export function zip<A, B>(self: IO<A>, b: IO<B>): IO<[A, B]> {
return IO(() => [self.io(), b.io()]);
}
只需添加额外的 JSDoc 注释,我们现在就可以使用运算符调用zip
两种IO
类型+
:
const zipped = IO.succeed(0) + IO.succeed(1);
并查看来自 VSCode 的快速信息:
此外,您可以通过指定运算符优先级来定义具有相同目标类型的多个运算符和多个流畅扩展。这可以实现简洁的 DSL,例如我们为 创建的 DSL @tsplus/stdlib/collections
。(顺便说一句,如果您在这里,您可能也应该安装@tsplus/stdlib
)。
您可以更详细地了解这些运算符在 TS+ 标准库测试套件中的用法。例如:List.test.ts(以及其他测试)。
最后一件事 - 我们还为自定义运算符实现了转到定义,因此您可以单击一个运算符并使用 IDE 的转到定义快速导航到该运算符的实现。
使用 TS+(惰性参数)
在 JavaScript/TypeScript 中,计算的惰性求值通常通过 thunk(即() => A
)实现。在很多情况下,延迟计算求值都非常有益,尤其是在我们想要避免对值进行急切计算的时候。这种“惰性”编程范式与效果系统结合使用时,会变得更加强大。
然而,如果你尝试用 JavaScript/TypeScript 编写一个真正懒惰的程序,你很快就会发现自己写了很多烦人的箭头函数,比如T.succeed(() => console.log("A"))
。这是因为我们希望效果系统能够控制 是否console.log
真正被调用。
为了避免这种情况,TS+ 允许我们将函数参数定义为“惰性”:
/**
* @tsplus type LazyArgument
*/
interface LazyArg<A> {
(): A
}
/**
* @tsplus static Effect/Ops succeed
*/
function succeed<A>(f: LazyArg<A>) {
f()
}
当调用时Effect.succeed(x)
,如果x
尚未进行惰性求值(即,如果x
还不是 thunk),编译器将把它转换为,Effect.succeed(() => x)
以便可以修剪恼人的样板。
因此类似于:
Effect.succeed(() => console.log("Hello, world!"))
变成
Effect.succeed(console.log("Hello, world!"))
请注意,该行为仅添加到具有值的类型标签的类型的函数参数LazyArgument
,通常不添加到任何函数参数。
使用 TS+(类型统一)
有没有遇到过这样的情况,但最终却Left<0> | Left<1> | Right<string>
发生了本该发生的事情Either<number, string>
?
这是因为 TypeScript 假定最右边的类型始终是最严格的(这是一个很好的假设,如果您手动输入它,它就会编译)。
不过,我们可以明确 TS+ 中的类型统一:
/**
* @tsplus type Option
*/
export type Option<A> = None | Some<A>;
/**
* @tsplus unify Option
* @tsplus unify Option/Some
* @tsplus unify Option/None
*/
export function unifyOption<X extends Option<any>>(
self: X
): Option<[X] extends [Option<infer A>] ? A : never> {
return self;
}
或者
/**
* @tsplus type Eval
*/
export interface Eval<A> {
readonly _A: () => A;
readonly _TypeId: unique symbol;
}
/**
* @tsplus unify Eval
*/
export function unifyEval<X extends Eval<any>>(self: X): Eval<[X] extends [Eval<infer AX>] ? AX : never> {
return self;
}
并且每次生成并集时,TS+ 都会使用统一函数的结果进行统一。
例如:
使用 TS+(全局导入)
如前所述,导入过多可能会非常麻烦。为了避免这种情况,我们发现许多 Effect 用户会定义自己的“prelude”或“utils”文件,用于重新导出常用模块。不幸的是,这通常会导致捆绑器使用的摇树优化算法出现极端情况,而这些捆绑器最近才开始改进对深层嵌套依赖树的摇树优化。
通过 TS+,我们使用全局导入的概念解决了这个问题。
.d.ts
全局导入是在声明文件( )中使用以下语法定义的导入:
/**
* @tsplus global
*/
import { Chunk } from "@tsplus/stdlib/collections/Chunk/definition";
当将类型定义为“全局”时,TS+ 会将该类型及其关联的构造函数/方法在项目中全局可用。但是,在编译期间,TS+ 会解析与该数据类型关联的构造函数/方法的用法,并将相关的导入添加到每个使用该数据类型的文件中。但请注意,只有当全局数据类型在文件中实际使用时,导入才会在编译期间添加到文件中。
通常的做法是prelude.d.ts
在根目录中定义一个,并将其添加到“tsconfig.json”的“files”部分。例如:
// tsconfig.json
{
"files": [
"./prelude.d.ts"
]
}
您还可以在您的项目之间共享您的前奏,就像我们对@tsplus/stdlib-global所做的那样,如果您导入您的前奏文件,它将允许您在项目中的任何地方访问完整的标准库。
使用 TS+(运行时类型和派生)
还以为不能再好了?关闭。
我们在应用开发中遇到的最大痛点之一就是定义运行时安全的类型。目前为止,我们尝试过很多解决方案,包括但不限于:io-ts
、、、、等等morphic-ts
。zod
@effect-ts/schema
@effect-ts/morphic
所有提到的库都很可爱,因为它们都试图解决一个巨大的问题——类型的编码、解码、保护(以及可能生成任意值)。
它们都使用了相同的技巧,不是定义一个类型,而是定义一个派生该类型的值。这些库之间的区别在于如何对这种类型的值进行建模的细节。
这导致了一系列的权衡,库最终可能会发出未优化的类型,变得冗长、难以扩展,有时使用起来相当痛苦。
需要明确的是,它们仍然比需要手动执行所有操作的替代方案要好。
经过数月的工作,我们认为我们终于找到了(有限/结构性)自定义类型类派生的解决方案,这样您就可以忘记这个问题了!
让我们深入研究一下:
export interface Person {
name: string;
age: Option<number>;
friends: Chunk<Person>;
}
export const PersonEncoder: Encoder<Person> = Derive();
export const PersonDecoder: Decoder<Person> = Derive();
export const PersonGuard: Guard<Person> = Derive();
就是这样,你现在可以这样做:
const encoded = PersonEncoder.encodeJSON({
name: "Mike",
age: Option.some(30),
friends: Chunk()
});
const decoded = PersonDecoder.decodeJSON(encoded);
if (decoded.isRight()) {
//
}
const maybePerson = {};
if (PersonGuard.is(maybePerson)) {
maybePerson.age;
}
你可能会问,编译器如何告诉你呢?在调用中添加一个“explain”参数,例如Derive
:
最好的事情是,上述 3 个实例没有什么特别之处,事实上所有规则都是自定义和可扩展的:
Guard.ts
Encoder.ts
Decoder.ts
每个可派生类型都定义为带有如下标签的接口:
/**
* A Guard<A> is a type representing the ability to identify when a value is of type A at runtime
*
* @tsplus type Guard
*/
export interface Guard<A> {
readonly is: (u: unknown) => u is A;
}
隐式实例定义为:
/**
* Guard for a number
*
* @tsplus implicit
*/
export const number: Guard<number> = Guard((u): u is number => typeof u === "number");
规则如下:
/**
* @tsplus derive Guard lazy
*/
export function deriveLazy<A>(
fn: (_: Guard<A>) => Guard<A>
): Guard<A> {
let cached: Guard<A> | undefined;
const guard: Guard<A> = Guard((u): u is A => {
if (!cached) {
cached = fn(guard);
}
return cached.is(u);
});
return guard;
}
/**
* @tsplus derive Guard<_> 10
*/
export function deriveLiteral<A extends string | number>(
...[value]: Check<Check.IsLiteral<A> & Check.Not<Check.IsUnion<A>>> extends Check.True ? [value: A] : never
): Guard<A> {
return Guard((u): u is A => u === value);
}
/**
* @tsplus derive Guard[Option]<_> 10
*/
export function deriveOption<A extends Option<any>>(
...[element]: [A] extends [Option<infer _A>] ? [element: Guard<_A>]
: never
): Guard<A> {
return Guard((u): u is A => {
if (u instanceof None) {
return true;
}
if (u instanceof Some) {
return element.is(u.value);
}
return false;
});
}
lazy
这里唯一特殊的规则是当编译器遇到递归推导时使用的规则,其余的都是自定义的。
规则是带有rule
标签的函数,其格式如下:
@tsplus derive Guard[Option]<_> 10
在哪里:
Guard
是我们想要派生的类型类的标签[Option]
允许您(如果没有省略)在应用规则时进一步扩大范围(在本例中为类似类型Show<Option<A>>
)<_>
告诉编译器如何调用函数参数,并且必须为每个我们要派生的类型类的泛型指定(例如Refinement<_,_>
),这可以是_
使用类型调用它,本例为“Option ”|
匹配一个联合体并用其成员的元组进行调用&
匹配一个交集,并用成员的元组进行调用[]
匹配一个元组并用其成员的元组进行调用
- 10 是优先级,定义此规则应用的时间
使用 TS+(模块结构和配置)
我们已经浏览了一系列示例,现在您可能已经注意到,我们倾向于使用完全限定的导入,"@tsplus/stdlib/collections/Chunk/definition"
甚至对于本地引用也是如此。
您不必这样做。但是,对于全局导入和扩展,您的文件通常也必须能够通过完全限定名称导入。“文件 => 完全限定导入”与“文件 => 跟踪名称”之间的映射定义在专用配置中,如下所示:tsplus.config.json。
需要跟踪图是因为在 TS+ 中您可以支持函数调用的编译时跟踪,例如如果您有如下函数:
function hello(message: string, __tsPlusTrace?: string) {
}
当发现类似的调用表达式时hello("world")
,如果没有明确传递跟踪,编译器就会用填充它hello("world", "file_trace_as_per_in_map:line:col")
。
虽然它并非始终存在(因为非 TS+ 用户显然不会使用编译器来填充跟踪信息),但一旦存在,它就可以成为构建易于调试系统的强大工具。例如,Effect 内部的跟踪系统允许渲染完整的堆栈跟踪信息,即使对于具有异步操作的程序也是如此。
使用 TS+(编译输出)
那么它在实践中是如何编译的呢?当导出的值带有 static/fluent/implicit/derivation 等标签时,当我们.d.ts
为每个函数生成定义文件时,我们会添加一个名为“location”的 jsdoc 注释,如下所示:
/**
* @tsplus fluent Decoder decode
* @tsplus location "@tsplus/stdlib/runtime/Decoder"
*/
export declare function decode<A>(decoder: Decoder<A>, value: unknown): import("../data/Either").Either<string, A>;
这就是我们知道如何导入东西以及从哪里导入东西的方式,在 JS 文件中,当使用类似的东西时,就会相应地引入导入。
这种设计确保模块始终尽可能靠近声明导入,并且有助于防止循环引用,它可能会导致编译js
文件中出现大量导入,但树状图可以很好地内联。
TS+ 的下一步计划
我们认为我们已经为第一个像样的版本做好了充分的功能准备,我们必须编写大量的测试并完成从效果代码库中提取标准库,然后才能在外部认为可以以适当的水平使用(内部效果已经用 TS+ 编写)但我们非常有信心,TS 人员在提供宝贵建议方面非常出色。
文章来源:https://dev.to/effect/the-case-for-ts-18b3