简体   繁体   English

在手写的 d.ts 文件中,如何从模块根目录中的一个命名空间公开函数?

[英]In a handwritten d.ts file, how can I expose functions from one namespace in the module root?

I'm working on a repo that's all in javascript but that exports handwritten type declarations ( automerge/index.d.ts ).我正在开发一个全部在 javascript 中但导出手写类型声明( automerge/index.d.ts )的存储库。

The structure of the codebase is that it has a Frontend and a Backend, plus a public API that offers some convenience functions of its own, in addition to re-exporting some functions directly from the Frontend and the Backend.代码库的结构是它有一个前端和一个后端,加上一个公共的API,它提供了自己的一些便利功能,此外还直接从前端和后端重新导出了一些功能。

Something like this:像这样的东西:

declare module `foo` {

  // functions that only exist in the public API
  function a
  function b
  function c

  // functions exposed directly from namespace A
  function q
  function r
  function s

  // functions exposed directly from namespace B
  function x
  function y
  function z

  namespace A {
    function q
    function r
    function s
    function t
  }

  namespace B {
    function v
    function w
    function x
    function y
    function z
  }

}

Here's an excerpt from the actual code showing how we're currently writing duplicate declarations for the re-exported functions.这是实际代码的摘录,显示了我们当前如何为重新导出的函数编写重复声明。

declare module 'automerge' {
  ...

  function getObjectById<T>(doc: Doc<T>, objectId: OpId): Doc<T>
  
  namespace Frontend {
    ...

    function getObjectById<T>(doc: Doc<T>, objectId: OpId): Doc<T>
  }

  ...
}

Is there a way to avoid writing these declarations twice?有没有办法避免两次编写这些声明?

I do not think what you are looking for is achievable with namespaces .我不认为你正在寻找的是可以通过命名空间实现的。 But namespaces are a legacy feature from very early days of Typescript and their usage is (strongly) discouraged - from official docs :但是命名空间是 Typescript 早期的遗留功能,并且(强烈)不鼓励使用它们 - 来自官方文档

[...] we recommended modules over namespaces in modern code. [...] 我们推荐在现代代码中使用模块而不是名称空间。

and shortly again:不久又一次:

Thus, for new projects modules would be the recommended code organization mechanism.因此,对于新项目,模块将是推荐的代码组织机制。

In case of providing type definition removing usage of namespaces should be relatively straightforward.在提供类型定义的情况下,删除命名空间的使用应该相对简单。

The easiest option would be to declare exported objects as such by directly declaring their types.最简单的选择是通过直接声明它们的类型来声明导出的对象。 In case of Frontend it would look something like that:Frontend的情况下,它看起来像这样:

const Frontend: {
    // in the main scope & Frontend
    // redeclared with typeof
    change: typeof change;
    emptyChange: typeof emptyChange;
    from: typeof from;
    getActorId: typeof getActorId;
    getConflicts: typeof getConflicts;
    getLastLocalChange: typeof getLastLocalChange;
    getObjectById: typeof getObjectById;
    getObjectId: typeof getObjectId;
    init: typeof init;

    // in Frontend only
    // declaration from scratch
    applyPatch<T>(
      doc: Doc<T>,
      patch: Patch,
      backendState?: BackendState
    ): Doc<T>;
    getBackendState<T>(doc: Doc<T>): BackendState;
    getElementIds(list: any): string[];
    setActorId<T>(doc: Doc<T>, actorId: string): Doc<T>;
  };

The above is not ideal as you need to type the exported function name twice, which is a bit error prone, but for amount of types you are dealing with probably totally fine.以上内容并不理想,因为您需要输入两次导出的 function 名称,这容易出错,但对于您处理的类型数量可能完全没问题。

The other option is to use auxiliary module to first group relative function together, then re-export them from auxiliary module and re-import from the main module:另一种选择是使用辅助模块首先将相关的 function 组合在一起,然后从辅助模块重新导出它们并从主模块重新导入:

declare module "automerge/frontend" {
  export {
    change,
    emptyChange,
    from,
    getActorId,
    getConflicts,
    getLastLocalChange,
    getObjectById,
    getObjectId,
    init
  } from "automerge";
  import { Doc, Patch, BackendState } from "automerge";

  export function applyPatch<T>(
    doc: Doc<T>,
    patch: Patch,
    backendState?: BackendState
  ): Doc<T>;
  export function getBackendState<T>(doc: Doc<T>): BackendState;
  export function getElementIds(list: any): string[];
  export function setActorId<T>(doc: Doc<T>, actorId: string): Doc<T>;
}

declare module "automerge" {
  /* other stuff */
  import * as _Frontend from 'automerge/frontend'
  const Frontend: typeof _Frontend
  /* other stuff */
}

The above is a bit convoluted and rather inelegant due to circular nature of imports/exports.由于进出口的循环性质,上述内容有点令人费解且相当不雅。 You could try to move all related functions to the module "automerge/frontend" , but then you would need re-export them from there, which will change slightly semantics and all export will need to be explicit (prefixed with export keyword - for example: export type Doc<T> = FreezeObject<T>; ).您可以尝试将所有相关功能移动到module "automerge/frontend" ,但是您需要从那里重新导出它们,这会稍微改变语义并且所有导出都需要显式(以export关键字为前缀 - 例如: export type Doc<T> = FreezeObject<T>; )。


As the most correct and future proof solution I could recommend refactoring the code into modules without any circular dependencies - probably it could require creating a common module for grouping shared types.作为最正确和面向未来的解决方案,我可以建议将代码重构为没有任何循环依赖关系的模块——可能需要创建一个通用模块来对共享类型进行分组。

Btw.顺便提一句。 if you interested in any of the above options please let me know and I would happily create a PR and we could move a discussion there.如果您对上述任何选项感兴趣,请告诉我,我很乐意创建一个 PR,我们可以在那里进行讨论。

This is simplified example, but you can achieve no duplication this way:这是简化的示例,但您可以通过这种方式实现不重复:

// backend.d.ts
declare module "backend" {
    export function Subtract(a: number, b: number): number;
}

Then:然后:

// foo.d.ts
declare module "foo" {
    export function Add(a: number, b: number): number;
    export * from "backend";
    export * as B from "backend";
}

And finally, the usage:最后,用法:

// main.ts
import * as foo from "foo";

foo.Add(1, 2); // defined only in the "foo".
foo.Subtract(1, 2); // "backend" function exposed in the root of "foo".
foo.B.Subtract(1, 2); // same "backend" function exposed in the "B" (namespace) of "foo".

One possibility would be to define an arrow function type alias and use that in both places.一种可能性是定义一个箭头 function 类型别名并在两个地方都使用它。 Eg:例如:

declare module "automerge" {
    type GetObjectById = <T>(doc: Doc<T>, objectId: OpId) => Doc<T>
    
    const getObjectById: GetObjectById

    namespace Frontend {
        const getObjectById: GetObjectById
    }
}

Unfortunately it is not possible to do the same directly with "regular" function declarations (see here ).不幸的是,不可能直接对“常规” function 声明做同样的事情(见这里)。

Arrow functions and function declarations are not exactly the same , especially around scoping of this within the function.箭头函数和 function 声明并不完全相同,尤其是围绕 function 中this范围。 For example arrow functions cannot have a this parameter:例如箭头函数不能有this参数:

// not allowed
const fn = (this: SomeContext) => void
// allowed
function fn(this: SomeConext): void

But if you are not relying on any features they differ on, or can just switch to arrow functions in your js code to be safe, then this should work.但是,如果您不依赖它们不同的任何功能,或者可以在 js 代码中切换到箭头函数以确保安全,那么这应该可以工作。

Something like this would partially help you:这样的事情会部分帮助你:

declare module 'automerge' {
  
  namespace Frontend {
    function getObjectById<T>(doc: T, objectId: any): T;
  }
  
  const getObjectById: typeof Frontend.getObjectById;

}

Try it on playground 在操场上试试

Pros:优点:

  • Reduces the amount of code by reusing typing of already declared functions.通过重用已声明函数的类型来减少代码量。

Cons:缺点:

  • Doesn't really eliminate the need of declaration const/function with the exact same name twice.并没有真正消除两次声明具有完全相同名称的 const/function 的需要。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM