Typescript: 允许模块实现接口

创建于 2014-08-10  ·  34评论  ·  资料来源: microsoft/TypeScript

当模块可以使用implements关键字实现接口时,这将很有用。 语法: module MyModule implements MyInterface { ... }

例:

interface Showable {
    show(): void;
}
function addShowable(showable: Showable) {

}

// This works:
module Login {
    export function show() {
        document.getElementById('login').style.display = 'block';
    }
}
addShowable(Login);

// This doesn't work (yet?)
module Menu implements Showable {
    export function show() {
        document.getElementById('menu').style.display = 'block';
    }
}
addShowable(Menu);
Suggestion help wanted

最有用的评论

一个用例是框架和工具,这些框架和工具在应用程序启动时扫描目录中的模块,并期望这些模块全部导出某种形状。

例如, Next.js扫描./pages/**/*.{ts,tsx}为您的页面模块,根据文件名生成路由。 确保每个模块导出正确的内容(由NextPage作为默认导出,以及可选的PageConfig导出名为config )由您决定:

import { NextPage, PageConfig } from 'next'

interface Props { userAgent?: string }

const Home: NextPage<Props> = ({ userAgent }) => (<main>...</main>)

Page.getInitialProps = async ({ req }) => {
  const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
  return { userAgent }
}

export default Page

export const config: PageConfig = {
  api: { bodyParser: false }
}

如果可以改为在顶部附近的一行中声明整个模块的输出形状,例如implements NextPageModule<Props> ,那将是很好的。

另一个想法:如果有某种方法可以在TypeScript配置中指定匹配某个模式(例如./pages/**/*.{ts,tsx} )的所有文件必须实现某种导出形状,这将很有趣,因此一个模块可以具有其导出类型-纯粹是因为它位于pages目录中而进行了检查。 但是我不确定这种方法是否有先例,它可能会引起混淆。

所有34条评论

外部模块如何使用? 人们一旦可以将其用于内部,也可能希望将其与外部一起使用。

这是个好问题。 我不知道哪种语法是最好的,但是这里有一些建议:

implements Showable; // I would prefer this one.
module implements Showable;
export implements Showable;

仅在不使用导出分配的外部模块上才允许使用它,因为如果使用导出分配,则导出的东西可能已经在另一个位置上具有implements

已批准。 我们更喜欢语法

export implements Showable;

并同意对于文件export =分配是不需要的。

还有一些问题:

  • 由于我们允许模块在声明站点中具有类型,因此我们也不应该允许它们在使用站点中也具有类型。 例如:
declare module "Module" implements Interface { }

import i : Interface = require("Module");
  • 您如何处理合并的声明,应该对所有声明的集合强制执行接口? 如果可见度不匹配怎么办?
    例如:
module Foo {
    export interface IBar {
        (a:string): void;
    }

    export module Bar implements IBar {  // should this be an error?
        export interface Interface {}
    }    

    function Bar(a: string) : void { }  // not exported
}

var bar: Foo.IBar = Foo.Bar;

应该在环境外部模块上允许使用。 在我看来,对于这些模块,应允许使用两种语法:

declare module "first" implements Foo { }
declare module "second"  {
  interface Bar { }
  export implements Bar; // this syntax is necessary, with the first syntax you can't reference Bar.  
}

还是Bar应该在{开头之前在Implements子句的范围内?

在我看来,将类型信息添加到import语句并不是真正有用的,因为您可以将类型信息添加到模块本身。

对于合并的声明,我想说的是,包含Implements子句的模块块应该实现接口。 这也可以防止可见性问题。

这与#2159有什么关系? 命名空间实现接口吗?

@jbondc如果我们这样做,它也将适用于名称空间。 您应该将内部模块和名称空间视为同构的。

您确定要走“名称空间”可以实现接口的实现路径吗?

哦,哇,这已经批准了一段时间了。 @RyanCavanaugh@DanielRosenwasser@mhegazy除非您有其他想法或调整,否则我可能会尽快实施。

我撤回了先前的怀疑态度,实际上我退出是因为它将带来新的结构性可能性。

与此相符,请考虑强制执行接口集合的接口,而不是仅强制声明实现的块-扩展名称空间/模块的本质,并包含许多重要的组件。 我希望能够使用它,但是我当然不想在同一文件中定义我的整个命名空间/模块。 在这种情况下为什么不只使用一个类呢?

@ Elephant-Vessel我不确定我们是在谈论模块,命名空间,包,功能还是...

@aluanhaddad是什么意思?

我的意思是,在这个讨论开始时,模块并不意味着今天的含义。 现在,我们使用名称空间一词来指代OP中描述为模块的内容,而模块具有更精确和不兼容的含义。 因此,当您谈论参与此实现的多个文件时,是指名称空间还是模块?

我指的是名称空间。 我想我只是想遵循这个线程的历史,对不起没有松动:)或者,当我想到它时,也许我脑中有了通用术语“模块”,描述了一个由set组成的更高级别的单元子组件组合在一起,以在系统中提供某些高级功能。 但是我只使用“命名空间”就可以了。

因此,我希望能够利用打字稿中的结构概念名称空间,对可以包含其他[_generic modules_]或类的[_generic modules_]进行描述和施加约束和期望。

我希望我们将能够更好地表达系统中更高层次的结构性期望。 类不能很好地扩展,它们可以很好地用作系统中的原子组件,但是我认为系统中的高层组织结构不能很好地用类来表达,因为它们被设计为可实例化和继承的,诸如此类。 太肿了。

我希望能用一种简单干净的方式来描述系统的高阶结构,不必大惊小怪。 优选地,唯一的大惊小怪是可选的方向可见性约束。 就像无法从_MySystem.Infrastructure_引用_MySystem.ClientApplication_一样,但反之亦然。 然后我们将开始令人兴奋的地方。

@ Elephant-Vessel感谢您的澄清。 我同意这将是非常有价值的,并且在这里,类类型不是正确的方法。 我认为您在谈论实例化时会触动头,因为名称空间代表的是概念上在库级别上是单例的事物。 尽管这不能强制执行,但从概念上讲,具有不_imply_多个实例化的内容将很有用。

我同意@ Elephant-Vessel。 虽然很容易将TypeScript误认为另一种Java,在Java中所有约束都是用单个类结构表示的,但TS具有更广泛的“ Shape”概念,该概念非常强大并消除了语义上的扭曲。 不幸的是,无法对模块施加约束会迫使开发人员退回类模式,以便更好地表达为模块。

例如,对于单元测试,能够在模块上表达一些“形状”(即约束)将非常有帮助,以便我们可以为特定的运行环境提供替代实现。 现在,看来以结构/检查的方式做到这一点的唯一方法是回到基于类的DI(例如la Spring),并使所有内容都成为一个类(并因此可实例化)。

无论如何,我的解释是@ Elephant-Vessel,但是如果我对TS有一个愿望,那就是这个。

这只鸟有话吗? 我也有这个问题

如此,不是一个简单的例子:

export {} as IFooBar;

该语法有什么问题? 我想语法已经被批准了,也许

export implements IFooBar

无论如何都期待着

这个已经入学了吗? 这将是一个很酷的功能

我们如何进行呢? 其功能强大。 乐于助人!

这个围栏有什么烦恼吗? 目前,我有一个问题是如何声明默认导出的接口。 例如:

export default {}

我想我可以做:

const x: MyInterface = {}
export default x;

这对于大多数TS文件都适用,但问题是,如果您先为JS编码并计划稍后再过渡到TS,那么效果就不好了。

我在想的另一件事是,实现的命名空间如何? 就像是:

export namespace Foo implements Bar {

}

我想Bar将是一个_abstract_名称空间哈哈idk

看到这个问题出现了很多次,我想我们都在寻找一件事:
在接口中支持静态成员。
如果发生这种情况,您可以只使用带有静态成员和接口的类,这与您在此处尝试执行的操作几乎相同,对吗?

无论哪种方式,都非常需要为接口添加静态支持或为模块添加接口支持。

@shiapetel不,不是那样。

我们做得到:

export default <T>{
  foo: Foo,
  bar: Bar
}

但这不是我们想要的。 我们专门在寻找:

export const foo : Foo = {};
export const bar : Bar = {};

但是目前没有机制可以强制模块导出foo和bar。 实际上,也没有机制可以强制模块导出正确的默认值。

如果接口支持静态成员,则可以使用具有静态foo / bar的类,该类继承自:
界面ILoveFooBar {
静态foo:FooType;
静态bar:BarType;
}

对?
这就是我的意思,我认为这对您的情况有所帮助-我知道这肯定对我有帮助。

接口的@shaipetel静态成员绝对可能有用,但对于此用例而言可能没有用。

这个问题是否只是在等待有人尝试实施?

一个用例是框架和工具,这些框架和工具在应用程序启动时扫描目录中的模块,并期望这些模块全部导出某种形状。

例如, Next.js扫描./pages/**/*.{ts,tsx}为您的页面模块,根据文件名生成路由。 确保每个模块导出正确的内容(由NextPage作为默认导出,以及可选的PageConfig导出名为config )由您决定:

import { NextPage, PageConfig } from 'next'

interface Props { userAgent?: string }

const Home: NextPage<Props> = ({ userAgent }) => (<main>...</main>)

Page.getInitialProps = async ({ req }) => {
  const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
  return { userAgent }
}

export default Page

export const config: PageConfig = {
  api: { bodyParser: false }
}

如果可以改为在顶部附近的一行中声明整个模块的输出形状,例如implements NextPageModule<Props> ,那将是很好的。

另一个想法:如果有某种方法可以在TypeScript配置中指定匹配某个模式(例如./pages/**/*.{ts,tsx} )的所有文件必须实现某种导出形状,这将很有趣,因此一个模块可以具有其导出类型-纯粹是因为它位于pages目录中而进行了检查。 但是我不确定这种方法是否有先例,它可能会引起混淆。

我发现当我只需要一个实现接口的简单模块时,我就经常想创建一个Singleton类。 任何提示如何最好地解决这个问题?

@RyanCavanaugh @DanielRosenwasser
我想解决这个问题。 您能给我一些解决方案的提示或在哪里看看吗?

从2020年的角度考虑这个问题,我想知道是否要重新使用type而不是export implements Showable type并允许export作为标识符? 今天,这是无效的语法,因此不太可能踩到任何人的现有代码库。

然后我们得到导入语法:

// Can re-use the import syntax
type export = import("webpack").Config

声明很容易编写:

// Can use normal literals
type export = { test: () => string, description: string }

// Generics are easy
type export = (props: any) => React.SFC<MyCustomModule>

还值得思考的是,等效的JSDoc也应该是:

/** <strong i="17">@typedef</strong> {import ("webpack").Config} export */

^中有一些注释-会议中有趣的一件事是,我们可以构建一个更通用的工具(用例而不是唯一的用例)的想法。

例如,如果我们有一个用于类型兼容性的类型断言运算符,则可以将其用于模块导出,并通常用于验证类型是否符合您的要求。 例如:

type assert is import("webpack").Config

const path = require('path');

export default {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
};

如果缺少目标,则意味着将其应用于顶级范围。 这可用于提供上下文类型输入(例如,您会在export default { en|获得自动完成功能

但在验证您自己的类型时也很有用:

import {someFunction} from "./example"

type assert ReturnType<typeof someFunction> is string

还值得思考的是,等效的JSDoc也应该是:
js /** <strong i="7">@typedef</strong> {import ("webpack").Config} export */

我认为@module将等同于JSDoc。 文件顶部应具有:
js /** <strong i="12">@module</strong> {import("webpack").Config} moduleName */

请参阅: https

故事书v6已更改为基于结构化模块的方法,它们称为“组件故事格式” 。 代码库中的所有.stories.js/ts模块都应包含类型Meta的默认导出。

无法以一种全局的方式表达这种期望,再加上在键入默认导出内容时存在的缺陷,使得将Storybook v6与TypeScript一起使用的体验比以前要平滑得多。

要添加到@jonrimmer的点,导出default这是有一定的type一种可复制的module将导致问题与树摇晃。

Webpack的import * as Foo没有问题。 但是,当您尝试对所有静态成员使用export default const = {}export default class ModuleName {进行相同操作时,未删除的导入不会被删除。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

dlaberge picture dlaberge  ·  3评论

CyrusNajmabadi picture CyrusNajmabadi  ·  3评论

weswigham picture weswigham  ·  3评论

fwanicka picture fwanicka  ·  3评论

MartynasZilinskas picture MartynasZilinskas  ·  3评论