Typescript: Adicione interfaces de suporte para definir métodos estáticos

Criado em 13 jan. 2017  ·  43Comentários  ·  Fonte: microsoft/TypeScript

Versão TypeScript: 2.0.3

Código

interface Foo {
public static myStaticMethod(param1: any);
}

Comportamento esperado:
Sem erros
Comportamento real:
Sem suporte

Question

Comentários muito úteis

@aluanhaddad Acho que seu código é uma solução alternativa para algo que é óbvio.

interface JsonSerializable {
      public static fromJson(obj: any);
      public toJson(): string;
}

Qual você vê com mais clareza?

Todos 43 comentários

O que você quer alcançar com isso? você pode elaborar sobre o cenário?

Imagino que abstract classes são o que você está procurando.

@mhegazy
Exemplo:

interface JsonSerializable {
      public static fromJson(obj: any);
      public toJson(): string;
}

@rozzzly Neste exemplo, abstract class não é válido.

Eu acho que isso seria muito legal! Java adicionou esta funcionalidade na última versão.

@Serginho
Acho que você pode achar isso interessante:

interface JsonSerializableStatic<C extends new (...args) => JsonSerializable<C>> {
  fromJson(json: string): JsonSerializable<C>;
}

interface JsonSerializable<C extends new (...args) => any> {
  toJson: () => string;
  constructor: C;
}

interface A extends JsonSerializable<typeof A> { }
class A implements JsonSerializable<typeof A> {

  constructor(readonly id: number, readonly name: string) { }
  toJson() { return JSON.stringify(this); }

  static fromJson(json: string): A {
    const obj = JSON.parse(json);
    return new A(obj.id, obj.name);
  }
}

const a = new A(1, 'Charlize');

const json = a.toJson();

const y = A.fromJson(json);
console.info(a, json, y);
console.info(new a.constructor(1, 'Theron'));
const m = new A.prototype.constructor(1, 'Charlize Theron');
console.info(m);

@aluanhaddad Acho que seu código é uma solução alternativa para algo que é óbvio.

interface JsonSerializable {
      public static fromJson(obj: any);
      public toJson(): string;
}

Qual você vê com mais clareza?

@aluanhaddad Acho que seu código é uma solução alternativa para algo que é óbvio.

interface JsonSerializable {
public static fromJson (obj: any);
public toJson (): string;
}
Qual você vê com mais clareza?

Na verdade, mas o meu não é uma solução alternativa, é uma justificativa para o seu caso de uso. Ter a desserialização estática e a serialização de instância implementada em uma base de classe por classe e, portanto, encapsulada e com segurança de tipo.
Sua declaração:

interface JsonSerializable {
     public static fromJson(obj: any);
     public toJson(): string;
}

é irrelevante, pois basicamente declara novamente a interface do objeto JSON. Isso absolutamente não requer métodos de interface estática de forma alguma.

Você está perdendo o conceito de Interface. class A implements JsonSerializable deve me fazer implementar os dois métodos. Mas realmente me faz implementar:

toJson: () => string;
constructor: new (...args) => JsonSerializableStatic<C>;

Não é uma solução clara.

Não há nenhuma razão técnica para não permitir a definição de métodos estáticos nas interfaces.

@Serginho Não estou discutindo que a situação seja ideal. Eu estava apenas tentando ilustrar que isso pode ser expresso.

@aluanhaddad Vamos! Abre a tua mente. Você acha que o texto digitado deve permitir métodos estáticos nas interfaces? isso foi implementado em Java 8 na última versão, então acho que não estou falando bobagem.

@Serginho Não acho que seja um ajuste particularmente bom para TypeScript. As interfaces devem definir a funcionalidade que um objeto fornece. Essa funcionalidade deve ser substituível e intercambiável (é por isso que os métodos de interface são virtuais). A estática é um conceito paralelo ao comportamento dinâmico / métodos virtuais. Entrelaçar os dois não parece certo do ponto de vista do design para mim.

Como @aluanhaddad já escreveu, o TypeScript na verdade já tem um mecanismo para expressar o que você deseja. A grande diferença é que, neste caso, você trata a classe como um objeto, que continua a ser logicamente consistente. Do meu ponto de vista, a abordagem que você propõe não é particularmente adequada para TypeScript (e desenvolvimento de JavaScript), pois as classes são uma espécie de hack / cidadãos de segunda classe. Os padrões atualmente prevalecentes contam com a programação de tipos de pato / formas, não em classes rígidas.

Acho que você também pode abrir sua mente e experimentar diferentes estilos de programação. O mundo não começa nem termina com OOP. Você pode achar a programação com funções, objetos simples e (e até certo ponto) protótipos agradável e ainda melhor. Esse estilo está muito mais de acordo com o que o JavaScript foi inicialmente projetado. Em uma nota lateral, concordo que JS está mudando lentamente para padrões OOP-esque (pelo menos no nível de comitê), mas isso é por causa do enorme empurrão de pessoas que não se sentem confortáveis ​​com diferentes paradigmas de programação e técnicas de desenvolvimento. Para mim é algo profundamente triste e decepcionante.

Como @gcnew disse, uma interface descreve a forma de um objeto individual, não sua classe. Mas não há razão para que não possamos usar uma interface para descrever a forma da própria classe, uma vez que as classes também são objetos.

import assert = require("assert");

interface JsonSerializableStatic<JsonType, InstanceType extends JsonSerializable<JsonType>> {
    fromJson(obj: JsonType): InstanceType;
}
interface JsonSerializable<JsonType> {
    toJson(): JsonType;
}

interface PointJson { x: number; y: number; }
class Point /*static implements JsonSerializableStatic<PointJson, Point>*/ {
    static fromJson(obj: PointJson): Point {
        return new Point(obj.x, obj.y)
    }

    constructor(readonly x: number, readonly y: number) {}

    toJson(): PointJson {
        return { x: this.x, y: this.y };
    }
}
// Hack for 'static implements'
const _: JsonSerializableStatic<PointJson, Point> = Point;

function testSerialization<JsonType, InstanceType extends JsonSerializable<JsonType>>(cls: JsonSerializableStatic<JsonType, InstanceType>, json: JsonType) {
    const instance: InstanceType = cls.fromJson(json);
    const outJson: JsonType = instance.toJson();
    assert.deepEqual(json, outJson);
}
testSerialization(Point, { x: 1, y: 2 });

Deixar de cumprir a assinatura de toJson ou fromJson resulta em um erro de compilação. (Infelizmente, o erro está em const _ e não no método.)

Provavelmente relacionado (já que lida com a digitação de métodos estáticos): # 5863.


@aluanhaddad : Seu exemplo tem um erro: JsonSerializable 's constructor membro realmente se referiria a uma propriedade de instância chamada constructor , que quando invocada com new retornar JsonSerializableStatic . O que isso significaria é que (new ((new X()).constructor)).fromJson({}) teria que funcionar. A razão de ser compilado com sucesso é que interface A extends JsonSerializable<typeof A> declara a implementação válida sem realmente verificá-la. Por exemplo, ele compila sem erros:

interface I { m(): void; }
interface A extends I { }
// No compile error
class A implements I { em() {} }

@Serginho Não é um usuário Java, mas não parece que a linguagem permite definir uma interface para o lado estático de uma classe (o que significa que as classes que implementam a interface teriam que implementar um método estático para se conformar). Java permite que você defina um método estático com um corpo em uma interface, o equivalente do TypeScript seria:

interface I { ... }
namespace I {
   export function interfaceStaticMethod() {}
}

E se você estivesse tentando fazer uma fábrica de genéricos?

interface Factorizable {
  static factory<U>(str: string): U
}

class Foo {
  private data: string[] = []
  bar<T extends Factorizable>(): T[] {
    return this.data.map(T.factory);
  }
}

class Bar implements Factorizable {
  static factory(str: string): Bar {
    // ...
  }
}

// Usage
var x = new Foo();
var y: Bar[] = x.bar();

Não tenho certeza sobre a sintaxe interface que estou propondo aqui, mas sei que a sintaxe de "uso" é o que estou tentando alcançar. Este é um padrão que uso com frequência em Swift (usando protocols ) e acho que seria muito bom em TypeScript. Embora não seja um designer de linguagem ou implementador de compilador, não tenho certeza se ele se encaixa na direção pretendida do TypeScript ou se é realista para ser implementado.

A classe não precisa implement a interface. você apenas define a interface, e as verificações de tipo estrutural armazenam em cache quaisquer problemas no local de uso, por exemplo:

interface Factorizable<U> {
    factory(str: string): U
}

class Foo {
  private data: string[] = []
  bar<T>(factory: Factorizable<T>): T[] {
    return this.data.map(factory.factory);
  }
}

class Bar {
  static factory(str: string): Bar {
    // ...
  }
}

// Usage
var x = new Foo();
var y = x.bar(Bar); // Bar[]

@mhegazy é uma solução relativamente boa. Obrigado por fornecer isso! 🙏

Ainda há duas coisas que me incomodam (reconhecidamente, nenhuma delas é um obstáculo):

  1. Ainda não podemos declarar que Bar explicitamente implementa ou está em conformidade com Factorizable .

    • Na prática, imagino que isso realmente não seja um problema. Visto que seria verdade que se a interface Factorizable mudasse de uma maneira incompatível, o uso x.bar(Bar) começaria a falhar e então você corrigiria as mudanças de desvio.
  2. Para mim, ainda é um fardo cognitivo declarar o tipo de y no lado direito da atribuição.

    • Mais estranho ainda, esta sintaxe permitiria este comportamento: var y: Baz[] = x.bar(Bar) . Obviamente, um erro, mas a sintaxe está permitindo ao desenvolvedor restringir demais o problema definindo o tipo de retorno em dois lugares.

Ainda não podemos declarar que Bar implementa explicitamente ou está em conformidade com Factorizable.

Existem dois tipos envolvidos, 1. função construtora (por exemplo, lado estático da classe) e 2. lado da instância (o que aparece ao chamar new ). Misturar esses dois em um tipo não é correto. Em teoria, você pode ter implements e static implements mas na prática, como você notou, isso raramente é usado e a cláusula implements realmente não adiciona muito. A verificação é feita no site de uso de qualquer maneira, independentemente se você tem uma cláusula implements ou não.

Para mim, ainda é um fardo cognitivo declarar o tipo de y no lado direito da atribuição.

Os dois significam coisas diferentes, var y = x.bar(Bar) declara uma nova variável y com o mesmo tipo de x.bar(Bar) ; onde var y: Bar[] = x.bar(Bar) declara uma nova variável y com o tipo Bar[] e verifica se o tipo de x.bar(Bar) ser atribuído a Bar[] .

Dito isso, é mais uma questão de estilo. Pessoalmente, minha recomendação é não usar anotações de tipo explicitamente, a menos que seja necessário; deixe os tipos fluírem através do sistema. Eu vi bases de código, no entanto, onde o guia de estilo é o oposto, onde tudo tem uma anotação de tipo explícita.

@mhegazy, obrigado pela discussão / perspectiva.

@ andy-hanson Obrigado por dedicar seu tempo para me corrigir. Corrigi o erro no meu exemplo.

Isso também funciona e mostra o erro em tempo de compilação, sem qualquer chamada de função extra:

interface Type<T> {
    new (...args: any[]): T;
}

/* static interface declaration */
interface ComparableStatic<T> extends Type<Comparable<T>> {
    compare(a: T, b: T): number;
}

/* interface declaration */
interface Comparable<T> {
    compare(a: T): number;
}

/* class decorator */
function staticImplements<T>() {
    return (constructor: T) => {}
}

@staticImplements<ComparableStatic<TableCell>>()   /* this statement implements both normal interface & static interface */
class TableCell { /* implements Comparable<TableCell> { */  /* not required. become optional */
    value: number;

    compare(a: TableCell): number {
        return this.value - a.value;
    }

    static compare(a: TableCell, b: TableCell): number {
        return a.value - b.value;
    }
}

Qual é o resultado da discussão?

Encontrei isso e também quero usar static interface :

interface IDb {
  public static instance: () => Db,
}

A maioria das pessoas esquece que já existem interfaces _static_ no sentido de que uma função / classe do construtor já tem duas interfaces, a interface do construtor e a interface da instância.

interface MyFoo {
  method(): void;
}

interface MyFooConstructor {
  new (): MyFoo;
  prototype: MyFoo;
  staticMethod(): any;
}

const MyFoo = function MyFoo() {
  this.prop = '';
} as any as MyFooConstructor;

MyFoo.prototype = {
  method() { console.log(this); }
}

MyFoo.staticMethod = function () { /* do something static */ }

Se você não está usando classes _abstract_, então você já tem o poder.

Obrigado @kitsonk por responder.

Sua declaração parece funcionar, mas é muito prolixa para o caso.

E eu apenas tentei classes _abstract_, mas parece que não suporta static com abstract .

[ts] 'static' modifier cannot be used with 'abstract' modifier.

@zixia, esse é o problema # 14600

Sim, vamos votar.

Alguém se importaria em responder com uma solução para minha pergunta aqui: http://stackoverflow.com/questions/44047874/dynamically-modify-typescript-classes-through-a-generic-function

Acho que todo esse problema seria resolvido com a capacidade de especificar membros estáticos nas interfaces e, se esse problema for fechado porque não é necessário , gostaria muito de ver como resolvê-lo.

@grantila Eu respondi sua pergunta. Conforme mencionado anteriormente neste problema, a menos que você tenha outros requisitos não mencionados lá, isso pode ser facilmente resolvido tratando as classes como objetos.

@ Enet4 Eu atualizei a pergunta, estava simplificada demais. O problema real não pode ser corrigido pelo hack Object.defineProperty() infelizmente. Que btw, é um hack. Quero garantir que não haja erros ortográficos de make - basicamente verificação estática adequada.

Este é um problema real que eu tenho, código que comecei a portar para o TypeScript, mas agora mantenho como JavaScript, já que atualmente não posso reescrever uma quantidade enorme de código que seria necessário de outra forma.

Quero garantir que não haja erros ortográficos de make - basicamente verificação estática adequada.

A verificação estática só pode ir até certo ponto aqui. Mas se sua única preocupação agora é definir a propriedade certa, então lançar para um tipo de criador e definir a propriedade de lá parece resolver isso.

@ Enet4 é uma solução de trabalho, obrigado. Acho que esse problema (13462) deve ser analisado novamente, pois soluções como a sua, usando conversão de tipo, na verdade não são seguras para tipos, e se essa for a única maneira de resolver a situação de trabalhar com o tipo de classe como um valor, nós está perdendo muito da flexibilidade de uma linguagem dinâmica.

soluções como a sua, usando conversão de tipo, na verdade não são seguras para tipos, e se essa for a única maneira de resolver a situação de trabalhar com o tipo de classe como um valor, estamos perdendo muito da flexibilidade de uma linguagem dinâmica.

@grantila Em minha defesa, isso é discutível. : wink: Seu caso de uso é diferente dos apresentados nesta edição, pois seu tipo de classe pode (ou não) fornecer um método dependendo das condições de tempo de execução. E IMO isso é mais inseguro do que o tipo cast apresentado na minha resposta, que só foi realizado para permitir a inserção de um campo em um objeto. Nesse sentido, o tipo de classe resultante C & Maker<T> deve permanecer compatível com todo o resto que depende de um C .

Também tentei imaginar onde os métodos estáticos em interfaces poderiam ajudá-lo aqui, mas pode estar faltando alguma coisa. Mesmo se você tivesse algo como static make?(... args: any[]): self em sua interface, ele teria que ser verificado em tempo de execução antes de uma chamada. Se você quiser continuar esta discussão, vamos considerar fazê-lo em outro lugar para reduzir o ruído. : ligeiramente_smiling_face:

Portanto, não podemos digitar métodos de fábrica estáticos de verificação em classes que implementam a mesma interface?

Meu caso de uso é:

interface IObject {
    static make(s: string): IObject;
}

class A implements IObject{
    static make(s: string): IObject {
        // Implementation A...
    }
}

class B implements IObject{
    static make(s: string): IObject {
        // Implementation B...
    }
}

A.make("string"); // returns A
B.make("string"); // returns B

Não quero escrever uma nova classe de fábrica apenas para isso.

@ tyteen4a03 Remova IObject daquele exemplo e ele irá compilar. Veja também https://github.com/Microsoft/TypeScript/issues/17545#issuecomment -319422545

@ andy-ms Sim, obviamente funciona, mas o objetivo da verificação de tipo é ... verificar os tipos. Você sempre pode degradar a segurança de tipo longe o suficiente para fazer todos os casos de uso compilarem, mas isso é ignorar o fato de que esta é uma solicitação de recurso e não tão maluca.

Sim, obviamente funciona, mas o objetivo da verificação de tipo é ... verificar os tipos.

Este é um longo e longo thread sobre como o lado estático da classe é uma interface separada do lado da instância e implements aponta para o lado da instância. @ andy-ms estava indicando a @ tyteen4a03 como fazer um trecho de código funcionar, porque era _errado_ não abrir mão da verificação de tipo.

Meu caso de uso para permitir que métodos estáticos usem o parâmetro genérico de classe é para classes mixin. Estou construindo uma estrutura de entidade que usa anotações para definir colunas, bancos de dados, etc. e gostaria de ter uma função estática misturada às minhas classes de entidade que permitiria acesso conveniente ao repositório digitado corretamente.

class RepositoryMixin<T> {
    public static repository(): EntityRepository<T> {
        return new EntityRepository<T>(Object.getPrototypeOf(this));
    }
}

@mixin(RepositoryMixin)
class Entity implements RepositoryMixin<Entity> {
    public id: number;
}

Entity.repository().save(new Entity());

@rmblstrp Você pode mostrar como usaria o recurso proposto em seu exemplo? De preferência como algo verificável?

@rmblstrp que não requer esse recurso. Na verdade, como você está usando um decorador, você pode usar _seu tipo_ para verificar se as classes anotadas fornecem os métodos estáticos necessários. Você não precisa de ambos e, na verdade, seria bastante redundante.

Olá, não quero sair do assunto ou do escopo desta conversa.
No entanto, uma vez que você trouxe à discussão diferentes paradigmas de programação (devemos usar OOP ou funcional?), Quero falar especificamente sobre fábricas estáticas que são usadas normalmente para criar uma conexão com um banco de dados ou fornecer algum tipo de serviço.
Em muitas linguagens como PHP e Java, as fábricas estáticas foram preteridas em favor da injeção de dependência. Os contêineres de injeção de dependência e IOC se tornaram populares graças a frameworks como Symfony e Spring.
Typescript tem um maravilhoso container IOC chamado InversifyJS.

Estes são os arquivos onde você pode ver como tudo é tratado.
https://github.com/Deviad/virtual-life/blob/master/models/generic.ts
https://github.com/Deviad/virtual-life/blob/master/service/user.ts
https://github.com/Deviad/virtual-life/blob/master/models/user.ts
https://github.com/Deviad/virtual-life/blob/master/utils/sqldb/client.ts
https://github.com/Deviad/virtual-life/blob/master/bootstrap.ts

Não estou dizendo que é uma solução perfeita (e provavelmente deixa alguns cenários descobertos), mas funciona também com o React, existem alguns exemplos que você já pode ver.

Além disso, sugiro que você assista a este vídeo sobre programação funcional que aborda esse aspecto sobre qual é o melhor: https://www.youtube.com/watch?v=e-5obm1G_FY&t=1487s
SPOILER: ninguém é melhor, depende do problema que você quer resolver. Se você lida com usuários, salas de aula, professores OOP será melhor modelar seu problema usando objetos.
Se você precisa de um analisador que varre um site, talvez seja melhor usar geradores de função que retornam resultados parciais, etc.

@Deviad @aluanhaddad Desde a minha postagem, tenho usado o InversifyJS e tem sido absolutamente ótimo e definitivamente um caminho muito melhor. No momento da minha postagem, eu tinha acabado de começar a usar Typescript / Node depois de ter sido PHP / C # anteriormente. Demorou um pouco para se familiarizar com o ambiente e os pacotes disponíveis.

Qual é o status disso? Por que você continua fechando questões não resolvidas no repo?

Acredito que este seja um daqueles casos em que o problema deve ser explicitamente rotulado como "wontfix", pois a escolha de não ter métodos estáticos nas interfaces é intencional.

@ enet4 , sou um recém-chegado, mas isso não ficou nada claro. Lendo este e outros problemas relacionados, parece que são principalmente os seguintes problemas:

A. é difícil
B. Não gostamos de (cada) sintaxe específica que vimos até agora.
C. Uma pequena minoria não acredita que isso deva ser factível de forma alguma, e prefere excluir a forma estranha atual de fazer isso.

Se for realmente intencional e os responsáveis ​​não quiserem, eles devem escrever um documento público e vinculá-lo a este e outros tópicos. Isso nos pouparia muito tempo ao nos manter no limbo.

Já vinculamos a # 14600 neste tópico e esse é o problema a ser seguido para a solicitação de recurso.

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

Questões relacionadas

DanielRosenwasser picture DanielRosenwasser  ·  3Comentários

manekinekko picture manekinekko  ·  3Comentários

fwanicka picture fwanicka  ·  3Comentários

blendsdk picture blendsdk  ·  3Comentários

bgrieder picture bgrieder  ·  3Comentários