TS +の場合


こんにちは!
それで、このポストは、何であるつもりですか?
さて、効果コミュニティでは、私たちは、何か新しいものについての最後の~ 6ヵ月の間、一生懸命働いていました.

目次


  • Why TS+
  • The Development of Fluent APIs
  • The Development of Pipeable APIs
  • Introducting TS+
  • Installing TS+
  • Fluent and Static Extensions
  • Call Extensions
  • Binary Operators
  • Lazy Arguments
  • Type Unification
  • Global Imports
  • Runtime Types and Derivation
  • Module Structure
  • Compiled Output
  • What's Next
  • なぜts +?

    Let's begin with a little bit of a back story. For the last four years (yeah, it's already been that long...), we have been working hard on the development of Effect .
    元々は、Scalaの開発者によってもたらされたアイデアに触発された小さなプロジェクトとして効果が始まった.しかし、それは徐々にSXIOのすべてのコアモジュールの本格的なポートに進化し、拡張モジュールの非常に豊富なセットと一緒にScalaで利用可能なデータ構造とTypesScriptで利用可能な間のギャップをブリッジします.
    すべてのこの仕事の間に、我々はTypesScript言語の大部分を愛して成長しました、そして、我々がどれくらい我々がすることができたかという非常に最初の日から、非常に驚きました、そして、言語が使用されていた開発者経験展望から、どれくらい安全で快適な.
    しかし、我々はまた、制限された量の制限に遭遇しました-我々が言語で何をすることができたかについて本当に制限されません、しかし、むしろ我々が言語で効率的にすることができたものは、冗長性のために、そして、生成されたコードの木のゆがみに関して.
    例から始めましょう.

    流暢なAPIの開発

    A fluent API in TypeScript might look something like:

    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();
      }
    }
    

    which can be used like:

    export const program = IO.succeed(0)
      .map((n) => n + 1)
      .flatMap((r) =>
        IO.succeedWith(() => {
          console.log(`result: ${r}`);
        })
      );
    
    program.run();
    

    In this approach we have a data-type IO<A> together with a "companion object" IO . This approach lends itself to a certain organization of code:

    • Constructors like succeed and succeedWith are placed in the "companion object" as static members
    • Methods like map , flatMap , and zip are placed in the class instance as members of the type IO<A>

    This approach is very common in object-oriented languages and its advantages lie in the ease of use. In fact, for the user of such a module, the editor experience is beautiful - if you want to construct an IO you type IO. and get a full list of suggestions on ways you can construct an IO . In addition, when you want to do something with a value of type IO<A> , you type value. and you are again prompted with a nice list of options.

    This fluent type of API is "a dream", but also has a few drawbacks that have led the biggest part of the FP community away from it.

    The limitations are staggering:

    • All methods have to be inside the class (or inside a common abstract class for ADTs)
    • Once a class is defined adding methods to it from the outside requires module augmentation and unsafe mutation of object prototypes
    • Worst of all, none of it can be optimized from a tree shaking perspective, so you'll end up with a huge bundle size that becomes prohibitive at a certain point

    The current "solution" that the FP community has adopted is the use of pipeable APIs.

    ピープ可能なAPIの開発

    In a pipeable API we would rewrite the prior example to:

    // 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);
    

    What we have essentially done is extracted all the constructors and methods outside of the class, and moved the this parameter to be a normal curried function parameter that the pipe function will carry through each function call.

    The resulting API is still quite nice visually, but it does have a few drawbacks.

    First of all, auto-imports do not work well with namespaced imports (recently folks at Unsplash have been ). Even if you do get auto-imports to work, you'll end up with tons of imports.

    Additionally, we have lost the meaning and categorization of IO - namely we no longer have a data-type IO<A> and a "companion object" IO . We now only have a data-type IO<A> and a module of functions that include both constructors, such as IO.succeed , and pipeable functions (which are also called aspects), such as IO.flatMap . So when programming a user needs to know exactly which module to import, exactly which functions are constructors of the datatype and which are methods for the datatype, and exactly how to use them.

    Finally, having had the experience of teaching those concepts to other developers, it seems fairly common that folks sometimes have issues reading pipeable function signatures.

    TSの紹介

    TS+ is a new language developed as a super-set of TypeScript and maintained as a fork of the original TypeScript compiler that is rebased daily.

    In order to guarantee full support with the TypeScript ecosystem, we limit what can be extended. TS+ emits standard declaration files consumable from plain TypeScript and consumes plain definition files. TS+ also does not add any new syntax.

    TS+ diverges from TypeScript in that we do not limit ourselves from emitting code using type information. Instead, we leverage the type information as much as possible to produce both highly optimized code and improve the developer experience.

    With those decisions comes a set of trade-offs. TS+ can be compiled only with the TS+ compiler (a fork of tsc ) and cannot be compiled by tools like babel which "simply" remove the types.

    We suggest using a compilation pipeline where tsc emits ES2022 modules, followed by another tool such as esbuild / swc / babel that takes over from there leveraging the powerful infrastructure of project references to optimize compilation.

    TSのインストール

    To install TS+, you must first add it as a dev dependency with:

    yarn add -D @tsplus/installer
    # or
    npm i -D @tsplus/installer
    

    Then, you can add a postinstall script to your package.json . For example:

    {
      "scripts": {
        "postinstall": "tsplus-install"
      }
    }
    

    This will replace the TypeScript that is installed in your node_modules with the TS+ version.

    Note: this install process is temporary until a better mechanism for installation is determined.

    After installing, you will want to also ensure that your IDE uses the TypeScript language service from the workspace and not the default one.

    If you want to start playing around with a pre-configured repository, you can open up https://github.com/ts-plus/starter-lib Gitpodで.

    TS +( fluentメソッド)の使用法

    Using our initial example, we will start by extracting all methods from the main IO class:

    /**
     * @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();
    }
    
    One thing that you may notice in the example above is that we have added a JSDoc annotation to our IO class. This annotation identifies the IO type in TS+ with a "type tag" of article.IO . The "type tag" for each type you define with TS+ should be globally unique. Prefixing type tags with your package's name is common, since non-prefixed tags should be reserved for the TS+ standard library .
    さて、私たちのタイプタグをIO クラスの中から各メソッドを関数に抽出することによってthis 何か他のものに(この場合、我々は使用を選びました)self ).
    さらに、各関数の上にJSDOC注釈を追加しました.この関数は、ts +の型で関数を関連付けます.例えば、flatMap 関数をIO データ型、注釈flatMap with @tsplus fluent article.IO flatMap . これは本質的に、コンパイラが「このstrent . 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) . あなたが望むどこにでも宣言することができます、そして、メソッドの経路はコンパイルの間、解決されます.
    piapable APIを好む人々をサポートするために、我々はまた、データ最初の1からpipeable機能を引き出すためにマクロを持っています.これは次のように使用できます.
    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());
    }
    
    これは問題の半分を解決するだけです.我々はデータ型からメソッドを抽出することができました、しかし、コンストラクタはまだ「コンパニオンオブジェクト」の静的メンバーです(非拡張性で非木のシェーカー可能です).
    次は解決しましょう
    /**
     * @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 注釈によるDataypeIO と一緒のクラス@tsplus companion article.IO/Ops . 次に、クラスからコンストラクタを抽出するには@tsplus static article.IO/Ops succeed 「この値をどんなタイプの静的メンバーとしてタグ付けしますか?」
    注意:タイプタグとコンパニオンで定義されているクラスとの間に違いはありませんが、クラスには2種類のインスタンス型とコンストラクタ型がありますので、どのタグをどのように関連付けるかを区別する方法が必要です.
    クラスを使用しないようにするための別のパターンは以下の通りです.
    /**
     * @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に簡単に発展する拡張可能な方法を提供します.実際には、ここで書かれているプログラムはIO モジュールを定義しました.

    TS +( call extension )の使用

    There are cases where we would like to add a call signature to something that isn't a function, for example we could refactor the above constructor like:

    /**
     * @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 };
    }
    

    This allows us to construct values for a datatype using the __call expression that we define. For example, we can create an IO<string> using the __call expression for the IO datatype above:

    const io: IO<string> = IO(() => "Hello, World!")
    

    The name __call is a special name that basically says "use the function as the call signature for a type". In the example above, TS+ will resolve the __call expression for the IO datatype to the make function that we defined for IO .

    In some extreme cases you may want access to the "this" and for that you can use a "fluent" variant of a __call expression instead of a "static" one.

    /**
     * @tsplus fluent article.IO/Ops __call
     */
    export function make<A>(self: IOOps, io: () => A): IO<A> {
      return { io };
    }
    

    You may think the set of features we have described thus far is enticing enough... ohh but we have only just started.

    TS +(バイナリ演算子)の使用

    There are many binary operators in JS - it's a shame there isn't a way of extending those (and the proposal to do so is inefficient in terms of tree shaking, limited, and may never gonna come to fruition)... Yeah, we can do that too :)

    Looking at the IO type that we defined above, the zip combinator that we defined for IO looks quite a bit like a binary operator. Given that the "+" symbol doesn't make any sense when used between two IO types in plain JavaScript/TypeScript, we can override it in 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()]);
    }
    

    With just that additional JSDoc annotation we can now call zip on two IO types using the + operator:

    const zipped = IO.succeed(0) + IO.succeed(1);
    

    And looking at the quick info from VSCode:


    さらに、演算子の優先順位を指定することによって、複数の演算子と同じターゲット型を持つ複数のfluent拡張子を定義できます.これは、私たちが作成したもののようなきちんとしたDSLの @tsplus/stdlib/collections . (もしあなたがここにいるなら、おそらくインストールするべきでしょう@tsplus/stdlib ).

    これらの演算子がTS +標準ライブラリのテストスイートでどのように使用されるかをより詳細に見ることができます.例えば、List.test.ts (他のテスト)
    つの最後のもの-私たちはまた、カスタム演算子の定義にGOを実装しているので、演算子をクリックして、IDEのGo to Definitionを使用して、その演算子の実装にすばやく移動できます.

    ts +(怠惰な引数)の使用

    In JavaScript/TypeScript, lazy evaluation of a computation is generally implemented via a thunk (i.e. () => A ). Deferring evaluation of computations can be quite beneficial in a variety of circumstances, particularly when we want to avoid eager computation of a value. This paradigm of "lazy" programming becomes even more powerful when combined with effect systems.

    However, if you attempt to write a truly lazy program in JavaScript/TypeScript, you'll quickly find yourself writing a lot of annoying arrow functions like T.succeed(() => console.log("A")) . This is because we want the effect system to assume control over whether or not the console.log is actually called.

    To avoid this, TS+ allows us to define function parameters as "lazy":

    /**
    * @tsplus type LazyArgument
    */
    interface LazyArg<A> {
      (): A
    }
    
    /**
     * @tsplus static Effect/Ops succeed
     */
    function succeed<A>(f: LazyArg<A>) {
      f()
    }
    

    When calling Effect.succeed(x) , if x is not already evaluated lazily (i.e. if x is not already a thunk), the compiler will transform it to Effect.succeed(() => x) making it possible to trim annoying boilerplate.

    So something like:

    Effect.succeed(() => console.log("Hello, world!"))
    

    becomes

    Effect.succeed(console.log("Hello, world!"))
    

    Note that the behavior is only added to function arguments of a type that has a type tag of value LazyArgument and not generally to any function arguments.

    TS +(タイプ単一化)の使用

    Ever ended up with something like Left<0> | Left<1> | Right<string> in something that should have been Either<number, string> ?

    That's because TypeScript assumes that the right-most type is always the strictest (which is a good assumption, if you manually type it, it compiles).

    We can be explicit about type unification in TS+ though:

    /**
     * @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;
    }
    

    or

    /**
     * @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;
    }
    

    and TS+ will use the result of the unify function to unify any time a union is generated.

    So that for example:


    TS +(グローバルインポート)の使用

    As mentioned before, having a lot of imports can be quite painful. To avoid this, we have found that many users of Effect, for example, define their own "prelude" or "utils" files that re-export commonly used modules. Unfortunately, this often leads to edge cases in tree-shaking algorithms used by bundlers that have only recently begin to improve shaking of deeply nested dependency trees.

    With TS+, we solve this problem using the concept of global imports.

    A global import is an import defined in a declaration file ( .d.ts ) using the following syntax:

    /**
     * @tsplus global
     */
    import { Chunk } from "@tsplus/stdlib/collections/Chunk/definition";
    

    When defining a type as "global", TS+ will make the type and its associated constructors/methods available globally in the project. However, during compilation TS+ will resolve usage of the constructors/methods associated with a datatype and add relevant imports to each file using the datatype. However, note that imports will only be added to a file during compilation if the global datatype is actually used in that file.

    It is a common practice to define a prelude.d.ts in your root and add it to the "files" portion of the "tsconfig.json". For example:

    // tsconfig.json
    {
      "files": [
        "./prelude.d.ts"
      ]
    }
    
    Also you can share your preludes across your projects like we do with @tsplus/stdlib-global それはあなたのプレリュードファイルにインポートする場合は、あなたのプロジェクトのすべての本格的な標準ライブラリへのアクセスを与える.

    TS +(ランタイム型と派生)の使用

    Thought it couldn't get any better? close.

    One of the biggest pain points we've ever experienced in app development is the definition of types that are safe at runtime. Many solutions have been attempted so far, including but not limited to: io-ts , morphic-ts , zod , @effect-ts/schema , @effect-ts/morphic , など
    すべての言及されたライブラリは、彼らがすべての巨大な問題を解決しようとしているので、美しいです-符号化、復号化、ガードして(そして、潜在的に仲裁を生成する)タイプ.
    それらはすべて、同じ型を使用します.型を定義する代わりに、型が派生した何らかの種類の値を定義します.ライブラリの違いは、その種類の値がどのようにモデル化されるかの詳細にあります.
    これにより、ライブラリが最適化されていない型を出力し、冗長で、拡張するのが困難になり、時には使用するのが非常に苦痛になることがあります.
    明確にするためには、すべてのマニュアル実装を必要とする代替案よりも優れています.
    仕事の数ヶ月後に我々は最終的に(限られた/構造)カスタムtypeclassの派生には、問題を忘れることができると考えている!
    掘り下げましょう
    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;
    }
    
    あなたはどのように尋ねることができますか?よく、コンパイラはあなたに言うことができます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 コンパイラが再帰的な派生に遭遇したときに使用され、残りはすべてカスタムです.
    ルールはAとの関数ですrule 以下の形式のタグ@tsplus derive Guard[Option]<_> 10
    ここで
  • Guard 入力するクラスクラスのタグです
  • [Option] ルールが適用された場合(これは省略されていない場合)さらにスコープを指定しますShow<Option<A>> )
  • <_> どのように関数引数をコールするかをコンパイラに通知しますRefinement<_,_> ), これは
  • _ それをタイプ、このケース"
  • | 組合とコールをメンバーのタプルでマッチする
  • & 交差点にマッチし、メンバーのタプルを呼び出します
  • [] タプルを呼び出し、メンバーのタプルを呼び出します
  • 10は、この規則がどれくらい早く適用されるかを定義する優先順位です
  • TS +(モジュールの構造と設定)

    We've been going trough a list of example and by now you probably noticed we tend to use fully qualified imports like "@tsplus/stdlib/collections/Chunk/definition" even for local references.

    You are not forced to do the same. However, for global imports and extensions in general your files must also be importable via a fully qualified name, the mapping between "file => fully qualified import" together with a map of "file => tracing name" is defined in a dedicated config like the following: tsplus.config.json .
    TS +では、関数呼び出しのコンパイル時追跡をサポートすることができます.例えば、以下のような関数がある場合には、トレースマップが必要です.
    function hello(message: string, __tsPlusTrace?: string) {
    }
    
    呼び出し式のようなときhello("world") トレースが明示的に渡されない場合には、コンパイラはhello("world", "file_trace_as_per_in_map:line:col") .
    これは常に存在するように頼ることができるものではありません(明らかに非TS +ユーザがトレースを埋めるコンパイラを持っていないので)、これが存在するとき、これは簡単にデバッグ可能なシステムを構築する強力なツールでありえます.例えば、効果の内部の追跡システムは、非同期操作を有するプログラムのためにさえ、完全なスタックトレースを表示することを可能にする.

    TS +(コンパイル済み出力)の使用

    So how does it compile in practice? When an exported value has a tag like static/fluent/implicit/derivation etc when we emit definition files .d.ts per each function we add a further jsdoc annotation called "location" like:

    /**
     * @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>;
    

    This is how we know how to import things and from where, in a JS file when something like that is used an import is introduced accordingly.

    This design ensures modules are always imported as close to the declaration as possible and it helps preventing circular references, it may lead to a big number of imports in the compiled js files but tree-shaking can inline very well.

    TS +の次は何ですか

    We think we are pretty much feature ready for the first decent release, we have to write tons of tests and finish extracting out the standard library from the effect codebase before this is considered usable at a decent level externally (internally effect is already written in TS+) but we feel very confident and the TS folks have been amazing in offering valuable advise.