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

问题描述

我正在开发一个全部在 javascript 中但导出手写类型声明 (automerge/index.d.ts) 的存储库。

代码库的结构是它有一个 Frontend 和一个 Backend,加上一个公共 API,除了直接从 Frontend 和 Backend 重新导出一些函数外,还提供了一些自己的便利功能

像这样:

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
  }

}

以下是实际代码的摘录,展示了我们目前如何为重新导出的函数编写重复声明。

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>
  }

  ...
}

有没有办法避免两次编写这些声明?

解决方法

我认为命名空间无法实现您正在寻找的内容。但是命名空间是 Typescript 早期的遗留功能,(强烈)不鼓励使用它们 - from official docs

[...] 我们推荐模块而不是现代代码中的命名空间。

很快又是:

因此,对于新项目,模块将是推荐的代码组织机制。

在提供类型定义的情况下,删除命名空间的使用应该相对简单。

最简单的选择是通过直接声明它们的类型来声明导出的对象。在 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>;
  };

以上并不理想,因为您需要输入两次导出的函数名称,这有点容易出错,但对于您处理的类型数量来说可能完全没问题。

另一种选择是使用辅助模块先将相关函数组合在一起,然后从辅助模块中重新导出,然后从主模块中重新导入:

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>,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 */
}

由于导入/导出的循环性质,上述内容有点复杂且不雅。您可以尝试将所有相关函数移动到 module "automerge/frontend",但随后您需要从那里重新导出它们,这将稍微改变语义,并且所有导出都需要是显式的(以 export 关键字为前缀- 例如:export type Doc<T> = FreezeObject<T>;).


作为最正确且面向未来的解决方案,我建议将代码重构为没有任何循环依赖的模块 - 可能需要创建一个通用模块来对共享类型进行分组。

顺便说一句。如果您对上述任何选项感兴趣,请告诉我,我很乐意创建一个 PR,我们可以在那里进行讨论。

,

这是简化的示例,但您可以通过这种方式实现不重复:

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

那么:

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

最后,用法:

// 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".
,

一种可能性是定义一个箭头函数类型别名并在两个地方使用它。例如:

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

    namespace Frontend {
        const getObjectById: GetObjectById
    }
}

不幸的是,不能直接使用“常规”函数声明来做同样的事情(参见 here)。

箭头函数和函数声明是 not exactly the same,尤其是在函数内 this 的范围。例如箭头函数不能有 this 参数:

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

但是如果您不依赖于它们不同的任何功能,或者可以在您的 js 代码中切换到箭头函数以确保安全,那么这应该可行。

,

这样的事情会部分帮助你:

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

}

Try it on playground

优点:

  • 通过重用已声明函数的类型来减少代码量。

缺点:

  • 并没有真正消除两次具有完全相同名称的声明 const/function 的需要。