TypeScript 识别TS中的JS命名空间模式

6jygbczu  于 6个月前  发布在  TypeScript
关注(0)|答案(3)|浏览(59)

简短地说,我提议两件事:

  1. 在TS文件中识别JavaScript命名空间模式,这些模式在JS文件中可以被--allowJs识别。例如var my = my || {}; my.app = my.app || {};(can't navigate javascript with manual namespaces #7632)。
  2. 从提供的字符串中识别创建全局JavaScript命名空间的方法。例如调用Ext.na("Company.data")应该具有与“手写”相同的效果。

为什么1

现在可以在项目中使用--allowJs标志,当你可以在JS文件和TS文件中定义这样的代码时:

  • JS文件*:
var app = app || {};
app.pages = app.pages || {};
app.pages.admin = app.pages.admin || {};

app.pages.admin.mailing = (function () {
    return {
        /**
* @param {string} email 
*/
        sendMail: function (email) { }
    }
})()
  • TS文件*:
(function(){
    app.pages.admin.mailing.sendMail("example@gmail.com")
})();

当我在TS文件的mailing属性上悬停时,可以看到我得到的结果。

问题是我想要将旧的JavaScript项目逐步迁移到Typescript,并将每个JS文件转换为TS。这并不那么容易或明显,要如何在不添加很多可以在带有allowJs编译器标志的情况下推断出类型定义的JS文件中迁移这种使用命名空间模式的JS文件。为什么不让Typescript文件识别这种模式呢?

为什么2 和示例

这是我当前项目在JS文件中实际使用的模式:

namespace('app.pages.admin');
app.pages.admin.mailing = (function () {
    return {
        /**
* @param {string} email 
*/
        sendMail: function (email) { }
    }
})()

namespace函数是全局函数,基本上创建了全局命名空间对象。它从逻辑上等同于从前面的例子中编写的var app = app || {}; app.pages = app.pages || {}; app.pages.admin = app.pages.admin || {};。我想能够在将JS文件转换为TS文件后使用这段代码(但具有Typescript提供的静态安全性)。我建议我们识别特殊的类型别名定义,这样编译器就会识别这种模式,并像对待这个全局“javascript命名空间”(或"expando"对象)一样行动。例如,这个类型别名将被添加到标准库中:

type JavaScriptNamespace = string;

然后我可以像这样声明全局的namespace函数:

declare function namespace(n : JavaScriptNamespace): void;

这样,TS代码将是有效的:

namespace('app.pages.admin');
app.pages.admin.mailing = (function () {
    return {
        sendMail: function (email: string): void { }
    }
})()

这有点类似于ThisType的精神。我的意思是编译器对某些类型的特殊处理。但是如果旧的编译器看到这种类型,那么什么都不会发生(它只是一个字符串类型)。

使用案例和当前的“解决方法”

这都是为了使现有的JavaScript项目的迁移到Typescript更容易。说服我的团队(甚至包括我自己)使用Typescript是很困难的,因为当我们需要编写这样的代码以获得与JS中的相同行为但具有类型安全性时:

//file: global.d.ts
declare var app: NamespaceApp
declare function namespace(namespace: string): void;

interface NamespaceApp {
    pages: NamespacePages;
}
interface NamespacePages {
    admin: NamespaceAdmin;
}
interface NamespaceAdmin {
}

//file: module1.js
interface NamespaceAdmin {
    module1: {
        foo: (arg: string) => void;
        bar: (arg: string[]) => void;
    }
}

namespace("app.pages.admin")
app.pages.admin.module1 = (function () {
    return {
        foo: function (arg: string) {
        },
        bar: function (arg: string[]) {
        }
    }
})()

//file: module2.js
interface NamespaceAdmin {
    module2: {
        foo2: (arg: string) => void;
        bar2: (arg: string[]) => void;
    }
}

namespace("app.pages.admin")
app.pages.admin.module2 = (function () {
    return {
        foo2: function (arg: string) {
        },
        bar2: function (arg: string[]) {
        }
    }
})()

你需要为你的每个方法编写两次类型定义,并将类型定义拆分为接口以合并它们🤮。我现在的解决方法是使用Typescript命名空间,如下所示(我有两种方法,都不喜欢):
这里有使用Typescript命名空间的许多问题:

  • 生成的代码更大,因为这就是Typescript命名空间的工作方式。
  • 第一种方法几乎看起来与原始JS代码相似,如果你可以使用“export”语法,就像在ES模块中那样。那么至少它会看起来与以前使用return的JS代码相似,就像在IIFE中使用的部分JavaScript命名空间一样。类似这样:
//second approach
namespace app5.pages.admin {
    function privateFunc(){}
    function sendMail(email: string): void{
        privateFunc();
    }   
    export { sendMail } //Error: "Export declarations are not permitted in a namespace.",
}

但这是不支持的。我认为现在没有必要改变它,因为现在命名空间并不常用。

  • 你基本上不能表示具有与命名空间部分相同名称的命名空间。例如,在JS中有`app.pages.app'命名空间,而我无法在TS中有它。最后我不得不使用不同的名称,我会在TS文件中使用这个名称,并使用解决方法使旧名称在其他JS文件中工作:
namespace app.pages.app {
    export const mailing = (function(){
        function privateFunc(){}
        function sendMail(email: string){
            app.pages.someOtherModule.foo(); //compiler error Property 'pages' does not exist on type 'typeof app'.
        }
        return {
            sendMail
        }
    })()    
}

总结

正如我提到的,这都是为了简化旧JavaScript项目(例如由于使用了ExtJS库)向Typescript的迁移。我知道现在这种模式并不那么流行,因为你应该使用ES模块。但我相信有很多人喜欢将他们的项目迁移到Typescript,但这很困难,因为这需要首先迁移到ES模块。而且这是一个巨大的任务本身。实际上,我的计划是用旧的命名空间向TS迁移当前的代码,然后尝试将其迁移到ES模块。当大多数代码都有类型时,迁移会更容易。你有更多的信心当编译器帮助你时。
如果TS团队认为只实现allowJS中的一个提案就足够了(只是识别在现有的命名空间中已经识别的内容,而不实现var Company = Company || {}; Company.data = Company.data || {};别名提案)对我来说就足够了,因为这比我目前的命名空间解决方法要好得多。但是如果有这个 --allowJs 别名功能的话,就可以将所有我的JS文件包含到TS编译中与mailing一起,这样这些JS代码就可以在TS中使用!(当然函数参数仍然是 allowJs ,但仍然)。我还会在当前的JS文件中获得更好的IntellSense,因为这个 namespace 函数将被识别为命名空间的创建者(在使用Salsa时)。

检查表单

我的建议符合以下准则:

  • 这不会破坏现有的TypeScript/JavaScript代码
  • 这不会改变现有JavaScript代码的运行时行为
  • 这可以在不根据表达式的类型发出不同JS的情况下实现
  • 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法等)
  • 这个特性将同意其余部分 xe3f1x 。
hivapdat

hivapdat1#

@sandersn的想法?我觉得这是一个检测扩展初始化的简单方法。

kupeojn6

kupeojn62#

如何难

Well, ThisType 被检查器识别为特殊项,而不是绑定器。特别是,知道 namespace 是一个具有 JavaScriptNamespace 类型参数的函数需要(1)绑定+名称解析(2)参数类型解析。绑定器可以立即伪造(1),但我不希望添加伪造支持(2)。
假设绑定器可以识别 JavaScriptNamespace 的使用,使用它支持命名空间声明将类似于支持 Object.defineProperty

这是个好主意吗

我不喜欢引入魔术标识符的想法,因为总有其他方法可以解决这个问题。我们三年前引入了 ThisType 以适应动态 JS 框架,这些框架接受一个带有方法的对象字面量并处理它,以便最终用不同的 this 调用这些方法。这在 JS 世界中很常见,当时我们认为这是必要的邪恶。今天,它是一个令人讨厌的遗留邪恶。我不确定这样声明命名空间是否那么常见,尤其是人们在过去几年转向模块。
我认为这个功能的可取程度的一个好指标是:有多少代码库使用它?它们使用得有多广泛?从 GitHub 上抓取示例和相对流行度可以帮助回答这个问题。

其他升级选项

虽然可能性不大,但你能生成代码来声明一个带有函数内部的命名空间并用扩展属性分配展开函数吗?

// generated code
declare namespace app.pages {
  declare function admin(): void
}
// original assignment
namespace("app.pages.admin");
app.pages.admin.mailing = (function () {
    return {
        sendMail: function (email: string): void { }
    }
})()
m4pnthwp

m4pnthwp3#

感谢大家的回复!
我希望TS支持某种“嵌套命名空间中的环境扩展对象”,@sandersn的工作解决方案提供了它。
这感觉有点hacky,如果Typescript在空的{}对象上支持类似的“扩展”模式,那么我可以使这个工作解决方案变得更好,就像这样:

declare namespace app.pages {
  const admin: {}
}
namespace("app.pages.admin");
app.pages.admin.mailing = (function () {
    return {
        sendMail: function (email: string): void { }
    }
})()

我还不明白为什么它不能仅仅使用对象字面量(不 Package 在IIFE中)就起作用。
我得到了'mailing' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.错误。
但这不是一个大问题。这个工作解决方案已经足够好了,解决了我大部分的问题。

相关问题