Typescript: Permitir que um módulo implemente uma interface

Criado em 10 ago. 2014  ·  34Comentários  ·  Fonte: microsoft/TypeScript

Seria útil quando um módulo pode implementar uma interface usando a palavra-chave implements . Sintaxe: module MyModule implements MyInterface { ... } .

Exemplo:

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

Comentários muito úteis

Um caso de uso seria para estruturas e ferramentas que varrem um diretório em busca de módulos na inicialização do aplicativo, esperando que todos esses módulos exportem um determinado formato.

Por exemplo, Next.js verifica ./pages/**/*.{ts,tsx} para os módulos de sua página, gerando rotas com base em seus nomes de arquivo. Cabe a você garantir que cada módulo exporte as coisas certas ( NextPage como exportação padrão e uma exportação opcional PageConfig denominada 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 }
}

Seria bom se você pudesse declarar a forma de exportação de todo o módulo em uma linha perto do topo, como implements NextPageModule<Props> .

Outro pensamento: seria interessante se houvesse alguma maneira de especificar em uma configuração do TypeScript que todos os arquivos que correspondem a um determinado padrão (como ./pages/**/*.{ts,tsx} ) devem implementar uma determinada forma de exportação, para que um módulo possa ter seu tipo verificado puramente porque está localizado no diretório pages , por exemplo. Mas não tenho certeza se existe algum precedente para essa abordagem, e pode ficar confuso.

Todos 34 comentários

Como isso funcionaria com módulos externos? É provável que, uma vez que as pessoas possam usá-lo para uso interno, também desejem usá-lo com externo.

Esta é uma boa pergunta. Não sei qual sintaxe seria a melhor, mas aqui estão algumas sugestões:

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

Só deve ser permitido em módulos externos que não usam uma atribuição de exportação, pois se você usar uma atribuição de exportação, o que você exporta já pode ter um implements em outro lugar.

Aprovado. Nós preferimos a sintaxe

export implements Showable;

e concordou que isso é desnecessário para arquivos export = atribuições.

Mais algumas perguntas:

  • Já que estamos permitindo que os módulos tenham tipos em sites de declaração, não devemos permitir que eles tenham tipos em sites de uso também. por exemplo:
declare module "Module" implements Interface { }

import i : Interface = require("Module");
  • O que você faz com as declarações mescladas, você deve impor a interface no agregado de todas as declarações? e o que acontece se eles não corresponderem em visibilidade?
    por exemplo:
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;

Deve ser permitido em módulos externos do ambiente. Para esses módulos, duas sintaxes devem ser permitidas em minha opinião:

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

Ou Bar deveria estar no escopo em uma cláusula de implementos antes da abertura { ?

Adicionar informações de tipo a uma instrução de importação não é realmente útil em minha opinião, já que você pode adicionar informações de tipo ao próprio módulo.

E para declarações mescladas, eu diria que o bloco de módulo que contém a cláusula implements deve implementar a interface. Isso também evita problemas de visibilidade.

Como isso estaria relacionado ao # 2159? Um namespace implementa uma interface?

@jbondc Se tivéssemos isso, também se aplicaria a namespaces. Você deve pensar em módulos internos e namespaces como isomórficos.

Tem certeza de que deseja seguir um caminho de implementação onde "namespaces" podem implementar interfaces?

Nossa, isso foi aprovado há um bom tempo. @RyanCavanaugh , @DanielRosenwasser , @mhegazy a menos que você tenha alguma

Retiro meu ceticismo anterior, na verdade saí para as novas possibilidades estruturais que ele traria.

Em linha com isso, considere impor a interface do agregado da interface em vez de apenas o bloco que declara a implementação - A natureza dos namespaces / módulos deve ser espalhada e conter muitos componentes não triviais. Gostaria de poder usar isso, mas certamente não quero definir todo o meu namespace / módulo no mesmo arquivo. Por que não usar apenas uma classe nesse caso?

@ Elephant-Vessel Não tenho certeza se estamos falando sobre Módulos, ou Namespaces, ou Pacotes, ou Recursos, ou ...

@aluanhaddad O que você quer dizer?

Quero dizer que no momento em que este módulo de discussão começou não significava o que significa hoje. Agora usamos o termo namespace para nos referirmos ao que é descrito no OP como um módulo, enquanto módulo assumiu um significado mais preciso e incompatível. Então, quando você fala sobre vários arquivos que fazem parte dessa implementação, você está se referindo a namespaces ou módulos?

Estou me referindo a namespaces. Acho que só queria me conformar com a história deste tópico, desculpe por não me soltar :) Ou, quando penso nisso, talvez eu tivesse o termo genérico 'módulo' em minha cabeça, descrevendo uma unidade de nível superior que consiste em um conjunto de subcomponentes, montados para fornecer certas funcionalidades de alto nível em um sistema. Mas estou bem em apenas escolher 'namespaces'.

Portanto, quero ser capaz de descrever e colocar restrições e expectativas em [_módulos genéricos_] que podem conter outros [_módulos genéricos_] ou classes, tirando proveito dos namespaces de conceito estrutural no texto digitado.

Minha esperança é que sejamos capazes de expressar melhor as expectativas estruturais de nível superior em um sistema. As classes não se adaptam bem, elas funcionam bem como componentes atômicos em um sistema, mas não acho que uma estrutura organizacional de nível superior em um sistema seja boa para se expressar com classes, pois são projetadas para serem instanciadas e herdadas e coisas assim . É muito inchado.

Eu apreciaria uma maneira simples e limpa de descrever a estrutura de ordem superior do sistema, sem complicações. De preferência, com o único barulho sendo restrições de visibilidade direcional opcionais. Como tornar impossível fazer referência a _MySystem.ClientApplication_ de _MySystem.Infrastructure_, mas vice-versa. Então começaríamos a ir a algum lugar excitante.

@ Elephant-Vessel obrigado por esclarecer. Eu concordo que isso seria extremamente valioso e que os tipos de classe não são a abordagem certa aqui. Acho que você acertou em cheio ao falar sobre instanciação porque os namespaces representam coisas que são conceitualmente singletons no nível da biblioteca. Embora isso não possa ser aplicado, seria útil conceitualmente ter algo que não _implique_ múltiplas instanciações.

Eu concordo com @ Elephant-Vessel. Embora seja fácil confundir o TypeScript com outro Java, onde todas as restrições são expressas com uma única estrutura de classe, o TS tem um conceito de "Forma" muito mais amplo que é muito poderoso e elimina o contortonismo semântico. Infelizmente, a incapacidade de colocar restrições no módulo tende a forçar os desenvolvedores a relegar de volta a um padrão de classe para coisas que seriam muito melhor expressas como módulo.

Por exemplo, para testes de unidade, seria muito útil ser capaz de expressar alguma "forma" (isto é, restrições) nos módulos para que possamos fornecer uma implementação alternativa para um contexto de execução específico. Agora, parece que a única maneira de fazer isso de forma estruturada / verificada é voltar para a DI baseada em classe (como la Spring) e tornar tudo uma classe (e, portanto, instanciável).

De qualquer forma, estou parafraseando @ Elephant-Vessel, mas se eu tenho um único desejo para TS, seria este.

Alguma palavra sobre este pássaro? Eu também tenho esse problema

tããão, uhh, não seria um simples caso de:

export {} as IFooBar;

o que há de errado com essa sintaxe? Acho que a sintaxe já foi aprovada, talvez como

export implements IFooBar

de qualquer maneira, ansioso por isso

Este matriculado / desembarcou ainda? isso vai ser um recurso legal

Como podemos progredir nisso? É incrivelmente poderoso. Fico feliz em ajudar!

qualquer worb neste birb? Uma dúvida que tenho no momento é como posso declarar uma interface para a exportação padrão. Por exemplo:

export default {}

Acho que posso apenas fazer:

const x: MyInterface = {}
export default x;

isso funcionaria para a maioria dos arquivos TS, mas o problema com isso é que, se você estiver codificando para JS primeiro e planejando fazer a transição para TS depois, isso não funcionará tão bem.

Outra coisa em que estava pensando, e quanto aos namespaces que implementam? Algo como:

export namespace Foo implements Bar {

}

Eu acho que Bar seria um namespace _abstract_ lol idk

Já vi essa pergunta surgir tantas vezes, e acho que todos estamos procurando apenas uma coisa:
Suporte a membros estáticos em uma interface.
Se isso acontecesse, você poderia apenas usar uma classe com membros estáticos e uma interface, que é quase a mesma coisa que você está tentando fazer aqui, certo?

De qualquer maneira, adicionar suporte estático a interfaces OU adicionar suporte de interface para módulos é altamente necessário.

@shiapetel nah não gosto disso.

nós podemos fazer isso:

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

mas não é isso que estamos procurando. estamos procurando especificamente por:

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

mas atualmente não há mecanismo para forçar o módulo a exportar foo e bar. E, de fato, não há mecanismo para garantir que o módulo exporte o valor padrão correto.

Se as interfaces suportassem membros estáticos, você poderia usar uma classe com foo / bar estático que herdou de:
Interface ILoveFooBar {
foo estático
barra estática
}

Direito?
Isso é o que eu quis dizer, acho que ajudaria na sua situação - eu sei que definitivamente ajudaria na minha.

Membros estáticos

Esse problema é apenas esperar que alguém tente a implementação?

Um caso de uso seria para estruturas e ferramentas que varrem um diretório em busca de módulos na inicialização do aplicativo, esperando que todos esses módulos exportem um determinado formato.

Por exemplo, Next.js verifica ./pages/**/*.{ts,tsx} para os módulos de sua página, gerando rotas com base em seus nomes de arquivo. Cabe a você garantir que cada módulo exporte as coisas certas ( NextPage como exportação padrão e uma exportação opcional PageConfig denominada 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 }
}

Seria bom se você pudesse declarar a forma de exportação de todo o módulo em uma linha perto do topo, como implements NextPageModule<Props> .

Outro pensamento: seria interessante se houvesse alguma maneira de especificar em uma configuração do TypeScript que todos os arquivos que correspondem a um determinado padrão (como ./pages/**/*.{ts,tsx} ) devem implementar uma determinada forma de exportação, para que um módulo possa ter seu tipo verificado puramente porque está localizado no diretório pages , por exemplo. Mas não tenho certeza se existe algum precedente para essa abordagem, e pode ficar confuso.

Muitas vezes fico tentado a criar uma classe Singleton quando um módulo simples que implementa uma interface é tudo de que preciso. Alguma dica da melhor forma de resolver isso?

@RyanCavanaugh @DanielRosenwasser
Eu quero trabalhar neste assunto. Você pode me dar algumas dicas para a solução ou onde procurar?

Pensando nisso de uma perspectiva de 2020, eu me pergunto se em vez de export implements Showable nós reutilizamos type e permitimos export como um identificador? Hoje, essa sintaxe é

Então, obtemos a sintaxe de importação:

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

As declarações são fáceis de escrever:

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

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

Também vale a pena pensar qual deveria ser o equivalente JSDoc, talvez:

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

Há algumas notas em ^ - uma coisa interessante que saiu da reunião foi a ideia de que poderíamos construir uma ferramenta mais genérica da qual este é um caso de uso, ao invés de a única coisa que ela faz.

Por exemplo, se tivéssemos um operador de asserção de tipo para compatibilidade de tipo, ele poderia ser usado para as exportações de módulo e, genericamente, para verificar se os tipos correspondem à sua preferência. Por exemplo:

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

Onde a falta de uma meta significa aplicá-la no escopo de nível superior. Isso pode ser usado para fornecer digitação contextual (por exemplo, você obteria o preenchimento automático no export default { en|

Mas também pode ser útil para validar seus próprios tipos:

import {someFunction} from "./example"

type assert ReturnType<typeof someFunction> is string

Também vale a pena pensar qual deveria ser o equivalente JSDoc, talvez:
js /** <strong i="7">@typedef</strong> {import ("webpack").Config} export */

Eu acho que @module seria o equivalente em JSDoc. A parte superior do arquivo deve ter:
js /** <strong i="12">@module</strong> {import("webpack").Config} moduleName */

Veja: https://jsdoc.app/tags-module.html

O Storybook v6 mudou para uma abordagem baseada em módulos estruturados que eles chamaram de Formato de História de Componentes . Espera-se que todos os módulos .stories.js/ts em uma base de código incluam uma exportação padrão com o tipo Meta .

Não ter como expressar essa expectativa de maneira global, combinada com a deficiência existente nas exportações padrão de digitação, torna o uso do Storybook v6 com TypeScript uma experiência muito menos tranquila do que poderia ser.

Para adicionar os pontos de @jonrimmer , exportar um default que é de um certo type que replica um module levará a problemas com o balanço da árvore.

Webpack não tem nenhum problema com a árvore balançando import * as Foo . Mas quando você tenta fazer o mesmo com export default const = {} ou export default class ModuleName { com todos os membros estáticos, as importações não utilizadas não são removidas.

Esta página foi útil?
0 / 5 - 0 avaliações