Typescript: Sugestão: Adicione métodos estáticos abstratos em classes e métodos estáticos em interfaces

Criado em 12 mar. 2017  ·  98Comentários  ·  Fonte: microsoft/TypeScript

Como um problema de continuação #2947, que permite o modificador abstract em declarações de métodos, mas não o permite em declarações de métodos estáticos, sugiro expandir essa funcionalidade para declarações de métodos estáticos, permitindo o modificador abstract static em declarações de métodos .

O problema relacionado diz respeito ao modificador static na declaração de métodos de interface, que não é permitido.

1. O problema

1.1. Métodos estáticos abstratos em classes abstratas

Em alguns casos de uso da classe abstrata e suas implementações, pode ser necessário ter alguns valores dependentes de classe (não dependentes de instância), que devem ser acessados ​​dentro do contexto da classe filha (não dentro do contexto de um objeto), sem criando um objeto. O recurso que permite fazer isso é o modificador static na declaração do método.

Por exemplo (exemplo 1):

abstract class AbstractParentClass {
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

FirstChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class FirstChildClass'
SecondChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class SecondChildClass'

Mas em alguns casos também preciso acessar esse valor quando só sei que a classe de acesso é herdada de AbstractParentClass, mas não sei qual classe filha específica estou acessando. Então, quero ter certeza de que todo filho da AbstractParentClass tenha esse método estático.

Por exemplo (exemplo 2):

abstract class AbstractParentClass {
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

abstract class AbstractParentClassFactory {

    public static getClasses(): (typeof AbstractParentClass)[] {
        return [
            FirstChildClass,
            SecondChildClass
        ];
    }
}

var classes = AbstractParentClassFactory.getClasses(); // returns some child classes (not objects) of AbstractParentClass

for (var index in classes) {
    if (classes.hasOwnProperty(index)) {
        classes[index].getSomeClassDependentValue(); // error: Property 'getSomeClassDependentValue' does not exist on type 'typeof AbstractParentClass'.
    }
}

Como resultado, o compilador decide que ocorreu um erro e exibe a mensagem: A propriedade 'getSomeClassDependentValue' não existe no tipo 'typeof AbstractParentClass'.

1.2. Métodos estáticos em interfaces

Em alguns casos, a lógica da interface implica que as classes implementadoras devem ter um método estático, que possui a lista predeterminada de parâmetros e retorna o valor do tipo exato.

Por exemplo (exemplo 3):

interface Serializable {
    serialize(): string;
    static deserialize(serializedValue: string): Serializable; // error: 'static' modifier cannot appear on a type member.
}

Ao compilar esse código, ocorre um erro: o modificador 'static' não pode aparecer em um membro do tipo.

2. A solução

A solução para ambos os problemas (1.1 e 1.2) é permitir o modificador abstract em declarações de métodos estáticos em classes abstratas e o modificador static em interfaces.

3. Implementação de JS

A implementação desse recurso em JavaScript deve ser semelhante à implementação de interfaces, métodos abstratos e métodos estáticos.

Isso significa que:

  1. Declarar métodos estáticos abstratos em uma classe abstrata não deve afetar a representação da classe abstrata no código JavaScript.
  2. Declarar métodos estáticos na interface não deve afetar a representação da interface no código JavaScript (não está presente).

Por exemplo, este código TypeScript (exemplo 4):

interface Serializable {
    serialize(): string;
    static deserialize(serializedValue: string): Serializable;
}

abstract class AbstractParentClass {
    public abstract static getSomeClassDependentValue(): string;
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass implements Serializable {

    public serialize(): string {
        var serialisedValue: string;
        // serialization of this
        return serialisedValue;
    }

    public static deserialize(serializedValue: string): SecondChildClass {
        var instance = new SecondChildClass();
        // deserialization
        return instance;
    }

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

deve ser compilado para este código JS:

var AbstractParentClass = (function () {
    function AbstractParentClass() {
    }
    return AbstractParentClass;
}());

var FirstChildClass = (function (_super) {
    __extends(FirstChildClass, _super);
    function FirstChildClass() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    FirstChildClass.getSomeClassDependentValue = function () {
        return 'Some class-dependent value of class FirstChildClass';
    };
    return FirstChildClass;
}(AbstractParentClass));

var SecondChildClass = (function (_super) {
    __extends(SecondChildClass, _super);
    function SecondChildClass() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    SecondChildClass.prototype.serialize = function () {
        var serialisedValue;
        // serialization of this
        return serialisedValue;
    };
    SecondChildClass.deserialize = function (serializedValue) {
        var instance = new SecondChildClass();
        // deserialization
        return instance;
    };
    SecondChildClass.getSomeClassDependentValue = function () {
        return 'Some class-dependent value of class SecondChildClass';
    };
    return SecondChildClass;
}(AbstractParentClass));

4. Pontos relevantes

  • A declaração de um método estático abstrato de uma classe abstrata deve ser marcada com o modificador abstract static
  • A implementação do método estático abstrato de uma classe abstrata deve ser marcada com o modificador abstract static ou static
  • A declaração do método estático de uma interface deve ser marcada com o modificador static
  • A implementação do método estático de uma interface deve ser marcada com o modificador static

Todas as outras propriedades do modificador abstract static devem ser herdadas das propriedades dos modificadores abstract e static .

Todas as outras propriedades do modificador de método de interface static devem ser herdadas dos métodos de interface e propriedades do modificador static .

5. Lista de verificação de recursos de idioma

  • Sintático

    • _Qual é a gramática deste recurso?_ - A gramática deste recurso é abstract static modificador de um método de classe abstrata e static modificador de um método de interface.

    • _Existe alguma implicação para a compatibilidade com JavaScript? Em caso afirmativo, eles são suficientemente mitigados?_ - Não há nenhuma implicação para a retrocompatibilidade do JavaScript.

    • _Esta sintaxe interfere nas alterações plausíveis do ES6 ou ES7?_ - Esta sintaxe não interfere nas alterações plausíveis do ES6 ou ES7.

  • Semântica

    • _O que é um erro no recurso proposto?_ - Agora há erros ao compilar o modificador abstract static de um método de classe abstrata e o modificador static de um método de interface.

    • _Como o recurso afeta os relacionamentos de subtipo, supertipo, identidade e designabilidade?_ - O recurso não afeta os relacionamentos de subtipo, supertipo, identidade e designabilidade.

    • _Como o recurso interage com genéricos?_ - O recurso não interage com genéricos.

  • Emitir

    • _Quais são os efeitos deste recurso na emissão de JavaScript?_ - Não há efeitos deste recurso na emissão de JavaScript.

    • _Isso emite corretamente na presença de variáveis ​​do tipo 'qualquer'?_ - Sim.

    • _Quais os impactos emitidos no arquivo de declaração (.d.ts)?_ - Não há impactos no arquivo de declaração.

    • _Esse recurso funciona bem com módulos externos?_ - Sim.

  • Compatibilidade

    • _Esta é uma alteração do compilador 1.0?_ - Provavelmente sim, o compilador 1.0 não conseguirá compilar o código implementando este recurso.

    • _Esta é uma mudança significativa do comportamento do JavaScript?_ - Não.

    • _Esta é uma implementação incompatível de um recurso JavaScript futuro (ou seja, ES6/ES7/posterior)?_ - Não.

  • De outros

    • _O recurso pode ser implementado sem afetar negativamente o desempenho do compilador?_ - Provavelmente sim.

    • _Qual o impacto em cenários de ferramentas, como preenchimento de membros e ajuda de assinatura em editores?_ - Provavelmente não tem nenhum impacto desse tipo.

Awaiting More Feedback Suggestion

Comentários muito úteis

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

Eu quero forçar a implementação do método deserialize estático nas subclasses de Serializable.
Existe alguma solução alternativa para implementar esse comportamento?

Todos 98 comentários

Métodos de interface estática geralmente não fazem sentido, veja #13462

Segurando isso até ouvirmos mais comentários sobre isso.

Uma questão importante que tivemos ao considerar isso: Quem tem permissão para chamar um método abstract static ? Presumivelmente, você não pode invocar AbstractParentClass.getSomeClassDependentValue diretamente. Mas você pode invocar o método em uma expressão do tipo AbstractParentClass ? Se sim, por que isso deveria ser permitido? Se não, qual é o uso do recurso?

Métodos de interface estática geralmente não fazem sentido, veja #13462

Na discussão de #13462 eu não vi porque os métodos de interface static são sem sentido. Só vi que sua funcionalidade pode ser implementada por outros meios (o que prova que não são sem sentido).

Do ponto de vista prático, a interface é um tipo de especificação - um determinado conjunto de métodos que são obrigatórios para sua implementação em uma classe que implementa essa interface. A interface não apenas define a funcionalidade que um objeto fornece, mas também é um tipo de contrato.

Portanto, não vejo nenhuma razão lógica para que class tenha o método static e interface não.

Se seguirmos o ponto de vista de que tudo o que já pode ser implementado na linguagem não precisa de melhorias de sintaxe e outras coisas (ou seja, este argumento foi um dos pontos principais na discussão do #13462), então guiado por este ponto de vista, podemos decidir que o ciclo while é redundante porque pode ser implementado usando for e if juntos. Mas não vamos acabar com while .

Uma questão importante que tivemos ao considerar isso: Quem tem permissão para chamar um método abstract static ? Presumivelmente, você não pode invocar AbstractParentClass.getSomeClassDependentValue diretamente. Mas você pode invocar o método em uma expressão do tipo AbstractParentClass ? Se sim, por que isso deveria ser permitido? Se não, qual é o uso do recurso?

Boa pergunta. Já que você estava considerando esse problema, você poderia compartilhar suas ideias sobre isso?

Só me vem à mente que no nível do compilador o caso de chamada direta de AbstractParentClass.getSomeClassDependentValue não será rastreado (porque não pode ser rastreado), e ocorrerá o erro de tempo de execução do JS. Mas não tenho certeza se isso é consistente com a ideologia do TypeScript.

Só vi que sua funcionalidade pode ser implementada por outros meios (o que prova que não são sem sentido).

Só porque algo é implementável, não significa que faça sentido. 😉

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

Eu quero forçar a implementação do método deserialize estático nas subclasses de Serializable.
Existe alguma solução alternativa para implementar esse comportamento?

Qual é a última opinião sobre isso? Eu apenas tentei escrever uma propriedade estática abstrata de uma classe abstrata e fiquei genuinamente surpreso quando isso não foi permitido.

O que @patryk-zielinski93 disse. Vindo de alguns anos de projetos em PHP que convertemos para TS, queremos métodos estáticos em interfaces.

Um caso de uso muito comum com o qual isso ajudará são os componentes React, que são classes com propriedades estáticas como displayName , propTypes e defaultProps .

Devido a essa limitação, as tipagens para React atualmente incluem dois tipos: uma classe Component ComponentClass incluindo a função construtora e propriedades estáticas.

Para verificar o tipo completo de um componente React com todas as suas propriedades estáticas, é preciso usar os dois tipos.

Exemplo sem ComponentClass : propriedades estáticas são ignoradas

import React, { Component, ComponentClass } from 'react';

type Props = { name: string };

{
  class ReactComponent extends Component<Props, any> {
    // expected error, but got none: displayName should be a string
    static displayName = 1
    // expected error, but got none: defaultProps.name should be a string
    static defaultProps = { name: 1 }
  };
}

Exemplo com ComponentClass : propriedades estáticas são verificadas por tipo

{
  // error: displayName should be a string
  // error: defaultProps.name should be a string
  const ReactComponent: ComponentClass<Props> = class extends Component<Props, any> {
    static displayName = 1
    static defaultProps = { name: 1 }
  };
}

Suspeito que muitas pessoas não estejam usando ComponentClass , sem saber que suas propriedades estáticas não estão sendo verificadas por tipo.

Problema relacionado: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16967

Há algum progresso nisso? Ou alguma solução alternativa para construtores em classes abstratas?
Aqui está um exemplo:

abstract class Model {
    abstract static fromString(value: string): Model
}

class Animal extends Model {
    constructor(public name: string, public weight: number) {}

    static fromString(value: string): Animal {
        return new Animal(...JSON.parse(value))
    }
}

@roboslone

class Animal {
    static fromString(value: string): Animal {
        return new Animal();
    }
}

function useModel<T>(model: { fromString(value: string): T }): T {
    return model.fromString("");
}

useModel(Animal); // Works!

Concordou que este é um recurso extremamente poderoso e útil. Na minha opinião, essa característica é o que torna as classes 'cidadãs de primeira classe'. A herança de métodos de classe/estáticos pode e faz sentido, particularmente para o padrão de fábrica de métodos estáticos, que foi citado aqui por outros pôsteres várias vezes. Esse padrão é particularmente útil para desserialização, que é uma operação executada com frequência no TypeScript. Por exemplo, faz todo o sentido querer definir uma interface que forneça um contrato declarando que todos os tipos de implementação são instanciados a partir de JSON.

Não permitir métodos de fábrica estáticos abstratos exige que o implementador crie classes de fábrica abstratas, duplicando desnecessariamente o número de definições de classe. E, como outros pôsteres apontaram, esse é um recurso poderoso e bem-sucedido implementado em outras linguagens, como PHP e Python.

Novo no Typescript, mas também estou surpreso que isso não seja permitido por padrão e que tantas pessoas estejam tentando justificar a não adição do recurso:

  1. O TS não precisa do recurso, porque você ainda pode realizar o que está tentando fazer por outros meios (o que é apenas um argumento válido se você fornecer um exemplo de uma maneira muito objetivamente melhor de fazer algo, como eu vi muito pouco)
  2. Só porque podemos não significa que devemos. Ótimo: mas as pessoas estão postando exemplos específicos de como isso seria útil/benéfico. Não vejo como poderia doer permitir isso.

Outro caso de uso simples: (maneira ideal, que não funciona)

import {map} from 'lodash';

export abstract class BaseListModel {
  abstract static get instance_type();

  items: any[];

  constructor(items?: any[]) {
    this.items = map(items, (item) => { return new this.constructor.instance_type(item) });
  }

  get length() { return this.items.length; }
}

export class QuestionList extends BaseListModel {
  static get instance_type() { return Question }
}

Em vez disso, a instância de lista acaba expondo o tipo de instância diretamente, o que não é relevante para a própria instância de lista e deve ser acessado por meio do construtor. Parece sujo. Pareceria muito mais sujo se estivéssemos falando de um modelo de registro, no qual especificamos um conjunto de valores padrão por meio do mesmo tipo de mecanismo etc.

Eu estava muito animado para usar classes abstratas reais em uma linguagem depois de estar em Ruby/javascript por tanto tempo, apenas para acabar desanimado com as restrições de implementação - O exemplo acima foi apenas meu primeiro exemplo de encontrá-lo, embora eu possa pensar em muitos outros casos de uso em que seria útil. Principalmente, apenas como um meio de criar DSL's/configurações simples como parte da interface estática, garantindo que uma classe especifique um objeto de valores padrão ou o que quer que seja. - E você pode estar pensando, bem, é para isso que servem as interfaces. Mas para algo simples como isso, não faz muito sentido, e só acaba tornando as coisas mais complicadas do que precisam ser (a subclasse precisaria estender a classe abstrata e implementar alguma interface, torna a nomeação das coisas mais complicada, etc).

Eu tive esse requisito semelhante para o meu projeto duas vezes. Ambos foram relacionados para garantir que todas as subclasses forneçam implementações concretas de um conjunto de métodos estáticos. Meu cenário está descrito abaixo:

class Action {
  constructor(public type='') {}
}

class AddAppleAction extends Action {
  static create(apple: Apple) {
    return new this(apple);
  }
  constructor(public apple: Apple) {
    super('add/apple');
  }
}

class AddPearAction extends Action {
  static create(pear: Pear) {
    return new this(pear);
  }

  constructor(public pear: Pear) {
    super('add/pear');
  }
}

const ActionCreators = {
  addApple: AddAppleAction
  addPear: AddPearAction
};

const getActionCreators = <T extends Action>(map: { [key: string]: T }) => {
  return Object.entries(map).reduce((creators, [key, ActionClass]) => ({
    ...creators,
    // To have this function run properly,
    // I need to guarantee that each ActionClass (subclass of Action) has a static create method defined.
    // This is where I want to use abstract class or interface to enforce this logic.
    // I have some work around to achieve this by using interface and class decorator, but it feels tricky and buggy. A native support for abstract class method would be really helpful here.
    [key]: ActionClass.create.bind(ActionClass)
  }), {});
};

Espero que isso possa explicar meus requisitos. Obrigado.

Para quem procura uma solução alternativa , você pode usar este decorador:

class myClass {
    public classProp: string;
}

interface myConstructor {
    new(): myClass;

    public readonly staticProp: string;
}

function StaticImplements <T>() {
    return (constructor: T) => { };
}

<strong i="7">@StaticImplements</strong> <myConstructor>()
class myClass implements myClass {}
const getActionCreators = <T extends Action>(map: { [key: string]: {new () => T, create() : T}}) => {

como estamos chamando por meio de um parâmetro de tipo, a classe base real com seu método de fábrica hipotético abstract static nunca estará envolvida. Os tipos das subclasses são relacionados estruturalmente quando o parâmetro type é instanciado.

O que não estou vendo nesta discussão são casos em que a implementação é invocada por meio do tipo da classe base em oposição a algum tipo sintetizado.

Também é relevante considerar que, diferentemente de linguagens como C#, onde os membros abstract são realmente _substituídos_ por suas implementações em classes derivadas, implementações de membros JavaScript, instância ou outra, nunca substituem números herdados, mas sim _sombra_ eles.

Meus 5 centavos aqui, do ponto de vista da perspectiva do usuário, são:

Para o caso de interfaces, deve ser permitido o modificador _static_

Interfaces definem contratos, a serem cumpridos pela implementação de classes. Isso significa que as interfaces são abstrações em um nível mais alto do que as classes.

Neste momento o que posso ver é que as classes podem ser mais expressivas que as interfaces, de forma que podemos ter método estático nas classes mas não podemos impor isso, a partir da própria definição do contrato.

Na minha opinião, isso parece errado e atrapalhando.

Existe algum histórico técnico que torna esse recurso de linguagem difícil ou impossível de implementar?

Felicidades

Interfaces definem contratos, a serem cumpridos pela implementação de classes. Isso significa que as interfaces são abstrações em um nível mais alto do que as classes.

As classes têm duas interfaces, dois contratos de implementação, e não há como fugir disso.
Existem razões pelas quais linguagens como C# também não possuem membros estáticos para interfaces. Logicamente, as interfaces são a superfície pública de um objeto. Uma interface descreve a superfície pública de um objeto. Isso significa que não contém nada que não esteja lá. Em instâncias de classes, métodos estáticos não estão presentes na instância. Eles só existem na função classe/construtor, portanto devem ser descritos apenas nessa interface.

Em instâncias de classes, métodos estáticos não estão presentes na instância.

você pode detalhar isso? Isso não é C#

Eles só existem na função classe/construtor, portanto devem ser descritos apenas nessa interface.

Que conseguimos e é isso que queremos mudar.

Interfaces definem contratos, a serem cumpridos pela implementação de classes. Isso significa que as interfaces são abstrações em um nível mais alto do que as classes.

Eu concordo plenamente com isso. Veja o exemplo de interface Json

Oi @kitsonk , por favor, você poderia elaborar mais sobre:

As classes têm duas interfaces, dois contratos de implementação, e não há como fugir disso.

Eu não entendi essa parte.

Logicamente, as interfaces são a superfície pública de um objeto. Uma interface descreve a superfície pública de um objeto. Isso significa que não contém nada que não esteja lá.

Eu concordo. Não vejo nenhuma contradição no que eu disse. Eu até disse mais. Eu disse que uma interface é um contrato para que uma classe seja cumprida.

Em instâncias de classes, métodos estáticos não estão presentes na instância. Eles só existem na função classe/construtor, portanto devem ser descritos apenas nessa interface.

Não tenho certeza se entendi direito, está claro o que diz, mas não por que você diz. Eu posso explicar porque eu acho que ainda é válida a minha declaração. Estou passando interfaces como parâmetros o tempo todo para meus métodos, isso significa que tenho acesso a métodos de interface, observe que não estou usando interfaces aqui como uma maneira de definir a estrutura de dados, mas para definir objetos concretos que são criados/hidratados em outro lugar . Então quando eu tenho:

fetchData(account: SalesRepresentativeInterface): Observable<Array<AccountModel>> {
    // Method Body
}

Nesse corpo de método, posso usar métodos account . O que eu gostaria que fosse possível fazer é poder usar métodos estáticos de SalesRepresentativeInterface que já foram aplicados para serem implementados em qualquer que seja a classe que estou recebendo em account . Talvez eu esteja tendo uma ideia muito simplista ou completamente errada sobre como usar o recurso.

Acho que permitir o modificador static me permitirá fazer algo como: SalesRepresentativeInterface.staticMethodCall()

Estou errado ?

Felicidades

@davidmpaz : sua sintaxe não está certa, mas está próxima. Aqui está um exemplo de padrão de uso:

interface JSONSerializable {
  static fromJSON(json: any): JSONSerializable;
  toJSON(): any;
}

function makeInstance<T extends JSONSerializable>(cls: typeof T): T {
  return cls.fromJSON({});
}

class ImplementsJSONSerializable implements JSONSerializable {
  constructor(private json: any) {
  }
  static fromJSON(json: any): ImplementsJSONSerializable {
    return new ImplementsJSONSerializable(json);
  }
  toJSON(): any {
    return this.json;
  }
}

// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable);

Infelizmente, para que isso funcione, precisamos de dois recursos do TypeScript: (1) métodos estáticos em interfaces e métodos estáticos abstratos; (2) a capacidade de usar typeof como uma dica de tipo com classes genéricas.

@davidmpaz

Eu não entendi essa parte.

As classes têm duas interfaces. A função construtora e o protótipo da instância. A palavra-chave class é essencialmente açúcar para isso.

interface Foo {
  bar(): void;
}

interface FooConstructor {
  new (): Foo;
  prototype: Foo;
  baz(): void;
}

declare const Foo: FooConstructor;

const foo = new Foo();
foo.bar();  // instance method
Foo.baz(); // static method

@jimmykane

você pode detalhar isso? Isso não é C#

class Foo {
    bar() {}
    static baz() {}
}

const foo = new Foo();

foo.bar();
Foo.baz();

Não há .baz() em instâncias de Foo . .baz() existe apenas no construtor.

De uma perspectiva de tipo, você pode referenciar/extrair essas duas interfaces. Foo refere-se à interface da instância pública e typeof Foo refere-se à interface do construtor público (que inclui os métodos estáticos).

Para acompanhar a explicação do @kitsonk , aqui está o exemplo acima reescrito para alcançar o que você deseja:

interface JSONSerializable <T>{
    fromJSON(json: any): T;
}

function makeInstance<T>(cls: JSONSerializable<T>): T {
    return cls.fromJSON({});
}

class ImplementsJSONSerializable {
    constructor (private json: any) {
    }
    static fromJSON(json: any): ImplementsJSONSerializable {
        return new ImplementsJSONSerializable(json);
    }
    toJSON(): any {
        return this.json;
    }
}

// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable); 

Observe aqui que:

  • A cláusula implements não é realmente necessária, toda vez que você usa a classe, uma verificação estrutural é feita e a classe será validada para corresponder à interface necessária.
  • você não precisa typeof para obter o tipo de construtor, apenas certifique-se de que seu T seja o que você pretende capturar

@mhegazy : você teve que remover minha chamada toJSON na interface JSONSerializable. Embora minha função de exemplo simples makeInstance chame apenas fromJSON , é muito comum querer instanciar um objeto e usá-lo. Você removeu minha capacidade de fazer chamadas sobre o que é retornado por makeInstance , porque eu realmente não sei o que T está dentro makeInstance (particularmente relevante se makeInstance é um método de classe usado internamente para criar uma instância, então use-o). Claro que eu poderia fazer isso:

interface JSONSerializer {
  toJSON(): any;
}

interface JSONSerializable <T extends JSONSerializer> {
  fromJSON(json: any): T;
}

function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
  return cls.fromJSON({});
}

class ImplementsJSONSerializer implements JSONSerializer {
  constructor (private json: any) {
  }
  static fromJSON(json: any): ImplementsJSONSerializer {
    return new ImplementsJSONSerializer(json);
  }
  toJSON(): any {
    return this.json;
  }
}

// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);

E agora eu sei que meu T terá todos os métodos disponíveis em JSONSerializer . Mas isso é excessivamente detalhado e difícil de raciocinar (espere, você está passando ImplementsJSONSerializer , mas isso não é um JSONSerializable , é? Espere, você está digitando de pato??) . O mais fácil de raciocinar sobre a versão é:

interface JSONSerializer {
  toJSON(): any;
}

interface JSONSerializable <T extends JSONSerializer> {
  fromJSON(json: any): T;
}

function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
  return cls.fromJSON({});
}

class ImplementsJSONSerializer implements JSONSerializer {
  constructor (private json: any) {
  }
  toJSON(): any {
    return this.json;
  }
}

class ImplementsJSONSerializable implements JSONSerializable<ImplementsJSONSerializer> {
  fromJSON(json: any): ImplementsJSONSerializer {
    return new ImplementsJSONSerializer(json);
  }

}

// returns an instance of ImplementsJSONSerializable
makeInstance(new ImplementsJSONSerializable());

Mas, como apontei em um comentário anterior, agora devemos criar uma classe de fábrica para cada classe que queremos instanciar, o que é ainda mais detalhado do que o exemplo de digitação de pato. Esse é certamente um padrão viável feito em Java o tempo todo (e presumo que C# também). Mas é desnecessariamente verboso e duplicativo. Todo esse clichê desaparece se métodos estáticos são permitidos em interfaces e métodos estáticos abstratos são permitidos em classes abstratas.

não há necessidade da classe adicional. classes podem ter membros static .. você só não precisa da cláusula implements . em um sistema de tipo nominal, você realmente precisa dele para afirmar o relacionamento "é um" em sua classe. em um sistema de tipo estrutural, você realmente não precisa disso. o relacionamento "é um" é verificado em cada uso de qualquer maneira, para que você possa descartar com segurança a cláusula implements e não perder a segurança.

então, em outras palavras, você pode ter uma classe que tenha um fromJSON estático e cujas instâncias tenham um toJSON :

interface JSONSerializer {
    toJSON(): any;
}

interface JSONSerializable<T extends JSONSerializer> {
    fromJSON(json: any): T;
}

function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
    return cls.fromJSON({});
}

class ImplementsJSONSerializer {
    constructor (private json: any) {
    }
    static fromJSON(json: any): ImplementsJSONSerializer {
        return new ImplementsJSONSerializer(json);
    }
    toJSON(): any {
        return this.json;
    }
}


// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);

@mhegazy Eu já indiquei isso como uma opção viável. Veja meu primeiro exemplo de 'digitação de pato'. Argumentei que é desnecessariamente verboso e difícil de raciocinar. Afirmei então que a versão mais fácil de raciocinar (classes de fábrica) é ainda mais detalhada.

Ninguém discorda que seja possível contornar a falta de métodos estáticos nas interfaces. Na verdade, eu diria que há uma solução mais elegante usando estilos funcionais em vez de classes de fábrica, embora essa seja minha preferência pessoal.

Mas, na minha opinião, a maneira mais limpa e fácil de raciocinar sobre a implementação desse padrão excepcionalmente comum é usar métodos estáticos abstratos. Aqueles de nós que adoram esse recurso em outras linguagens (Python, PHP) sentem falta de tê-lo no TypeScript. Obrigado pelo seu tempo.

Afirmei então que a versão mais fácil de raciocinar (classes de fábrica)

não tenho certeza se concordo que isso é mais fácil de raciocinar.

Mas, na minha opinião, a maneira mais limpa e fácil de raciocinar sobre a implementação desse padrão excepcionalmente comum é usar métodos estáticos abstratos. Aqueles de nós que adoram esse recurso em outras linguagens (Python, PHP) sentem falta de tê-lo no TypeScript.

E esse problema está rastreando a adição disso. Eu só queria garantir que os futuros visitantes deste tópico entendam que isso é possível hoje sem uma cláusula implements .

@kitsonk certo, eu perdi isso.

Pessoal, então eu tenho como LocationModule, que deve ter método para obter opções para armazenar em banco de dados, a partir do qual ele deve se recriar.
Cada LocationModule tem suas próprias opções com tipo para recreação.

Então agora eu tenho que criar uma fábrica com genéricos para recreação e mesmo assim não tenho verificações de tempo de compilação. Você sabe, batendo o propósito aqui.

No começo eu estava tipo "bem, sem estática para interfaces, então vamos ficar com a classe abstrata, MAS até isso seria sujo, já que agora eu tenho que mudar em todos os lugares do meu tipo de código de interface para classe abstrata para que o verificador reconheça os métodos que estou procurando .

Aqui:

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
}

export abstract class AbstractLocationModule implements LocationModuleInterface {
  abstract readonly name: string;

  abstract getRecreationOptions(): ModuleRecreationOptions;

  abstract static recreateFromOptions(options: ModuleRecreationOptions): AbstractLocationModule;
}

E então eu tropeço bem, estático não pode ser com abstrato. E não pode estar na interface.

Pessoal, sério, não estamos protegendo não implementar isso apenas por não implementar isso?

Eu amo TypeScript apaixonadamente. Como louco, quero dizer. Mas há sempre mas.

Em vez de forçar a implementação do método estático, eu teria que verificar novamente como estava fazendo com tudo em JavaScript simples e antigo.

A arquitetura já está cheia de padrões, decisões legais etc, mas alguns deles são apenas para superar problemas com o uso de coisas tão simples como o método estático da interface.

Minha fábrica terá que verificar em tempo de execução a existência do método estático. Como é isso?

Ou melhor ainda:

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
  dontForgetToHaveStaticMethodForRecreation();
}

Se você estiver usando o objeto de classe polimorficamente, que é a única razão para ele implementar uma interface, não importa se as interfaces que especificam os requisitos para o lado estático da classe são referenciadas sintaticamente pela própria classe porque será verificado no site de uso e emitirá um erro de tempo de design se a(s) interface(s) necessária(s) não for(em) implementada(s).

@malina-kirn

Veja meu primeiro exemplo de 'digitação de pato'.

Esse problema não tem relação com o uso ou não da tipagem de pato. TypeScript é do tipo pato.

@aluanhaddad Sua implicação não está correta. Eu posso entender seu ponto de que a função primária implements ISomeInterface é para usar o objeto de classe polimorficamente. No entanto, pode importar se a classe referencia a(s) interface(s) que descrevem a forma estática do objeto de classe.

Você implica que a inferência de tipo implícita e os erros do site de uso abrangem todos os casos de uso.
Considere este exemplo:

interface IComponent<TProps> {
    render(): JSX.Element;
}

type ComponentStaticDisplayName = 'One' | 'Two' | 'Three' | 'Four';
interface IComponentStatic<TProps> {
    defaultProps?: Partial<TProps>;
    displayName: ComponentStaticDisplayName;
    hasBeenValidated: 'Yes' | 'No';
    isFlammable: 'Yes' | 'No';
    isRadioactive: 'Yes' | 'No';
    willGoBoom: 'Yes' | 'No';
}

interface IComponentClass<TProps> extends IComponentStatic<TProps> {
    new (): IComponent<TProps> & IComponentStatic<TProps>;
}

function validateComponentStaticAtRuntime<TProps>(ComponentStatic: IComponentStatic<TProps>, values: any): void {
    if(ComponentStatic.isFlammable === 'Yes') {
        // something
    } else if(ComponentStatic.isFlammable === 'No') {
        // something else
    }
}

// This works, we've explicitly described the object using an interface
const componentStaticInstance: IComponentStatic<any> = {
    displayName: 'One',
    hasBeenValidated: 'No',
    isFlammable: 'Yes',
    isRadioactive: 'No',
    willGoBoom: 'Yes'
};

// Also works
validateComponentStaticAtRuntime(componentStaticInstance, {});

class ExampleComponent1 implements IComponent<any> {
    public render(): JSX.Element { return null; }

    public static displayName = 'One'; // inferred as type string
    public static hasBeenValidated = 'No'; // ditto ...
    public static isFlammable = 'Yes';
    public static isRadioactive = 'No';
    public static willGoBoom = 'Yes';
}

// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent1, {});

class ExampleComponent2 implements IComponent<any> {
    public render(): JSX.Element { return null; }

    public static displayName = 'One';
    public static hasBeenValidated = 'No';
    public static isFlammable = 'Yes';
    public static isRadioactive = 'No';
    public static willGoBoom = 'Yes';
}

// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent2, {});

No exemplo acima, é necessária uma declaração de tipo explícita; não descrever a forma do objeto de classe (lado estático) resulta em um erro de tempo de design no site de uso, mesmo que os valores reais estejam em conformidade com a forma esperada.

Além disso, a falta de uma maneira de referenciar a(s) interface(s) descrevendo o lado estático, nos deixa com a única opção de declarar explicitamente o tipo em cada membro individual; e depois repetir isso em todas as aulas.

Com base no meu post anterior, sinto que o modificador static nas interfaces é uma solução muito boa para alguns casos de uso que precisam ser resolvidos. Peço desculpas pelo tamanho deste comentário, mas há muito que quero ilustrar.

1) Há momentos em que precisamos descrever explicitamente a forma do lado estático de um objeto de classe, como no meu exemplo acima.
2) Há momentos em que a verificação de forma no site de definição é altamente desejável, e momentos como no exemplo de @OliverJAsh em que o site de uso está em código externo que não será verificado e você precisa verificar a forma no site de definição .

Em relação ao número 2, muitos posts que li sugerem a verificação de forma, pois o site de uso é mais que suficiente. Mas nos casos em que o site de uso está em um módulo de galáxia muito, muito distante... ou quando o site de uso está em um local externo que não será verificado, isso obviamente não é o caso.

Outros posts sugerem #workarounds para verificar a forma no site de definição. Embora essas temidas soluções alternativas permitam que você verifique a forma no local de definição (em alguns casos), existem problemas:

  • Eles não resolvem o número 1 acima, você ainda precisa declarar explicitamente o tipo em cada membro em cada classe.
  • Em muitos casos, falta clareza, elegância e legibilidade. Eles nem sempre são fáceis de usar, não são intuitivos e podem demorar um pouco para descobrir que são possíveis.
  • Eles não funcionam em todos os casos e encontrar #workaroundsToMakeTheWorkaroundsWork não é divertido... ou produtivo... mas principalmente não é divertido.

Aqui está um exemplo da solução alternativa que vi recentemente para forçar a verificação de forma no site de definição...

// (at least) two separate interfaces
interface ISomeComponent { ... }
interface ISomeComponentClass { ... }

// confusing workaround with extra work to use, not very intuitive, etc
export const SomeComponent: ISomeComponentClass =
class SomeComponent extends Component<IProps, IState> implements ISomeComponent {
...
}

Agora tente usar essa solução alternativa com uma classe abstrata lançada

interface ISomeComponent { ... }
// need to separate the static type from the class type, because the abstract class won't satisfy
// the shape check for new(): ... 
interface ISomeComponentStatic { ... }
interface ISomeComponentClass { 
    new (): ISomeComponent & ISomeComponentStatic;
}

// I AM ERROR.    ... cannot find name abstract
export const SomeComponentBase: ISomeComponentStatic =
abstract class SomeComponentBase extends Component<IProps, IState> implements ISomeComponent {
...
}

export const SomeComponent: ISomeComponentClass =
class extends SomeComponentBase { ... }

Vamos, acho que vamos contornar isso também... suspiro

...

abstract class SomeComponentBaseClass extends Component<IProps, IState> implements ISomeComponent { 
   ... 
}

// Not as nice since we're now below the actual definition site and it's a little more verbose
// but hey at least were in the same module and we can check the shape if the use site is in
// external code...
// We now have to decide if we'll use this work around for all classes or only the abstract ones
// and instead mix and match workaround syntax
export const SomeComponentBase: ISomeComponentStatic = SomeComponentBaseClass;

Mas espere, há mais! Vamos ver o que acontece se estivermos usando genéricos ....

interface IComponent<TProps extends A> { ... }
interface IComponentStatic<TProps extends A> { ... }

interface IComponentClass<TProps extends A, TOptions extends B> extends IComponentStatic<TProps> {
    new (options: TOptions): IComponent<TProps> & IComponentStatic<TProps>;
}

abstract class SomeComponentBaseClass<TProps extends A, TOptions extends B> extends Component<TProps, IState> implements IComponent<TProps> {
...
}

// Ruh Roh Raggy: "Generic type .... requires 2 type argument(s) ..."
export const SomeComponentBase: IComponentStatic = SomeComponentBaseClass;


// "....  requires 1 type argument(s) ... "    OH NO, We need a different workaround
export const SomeComponent: IComponentStatic =
class extends SomeComponentBase<TProps extends A, ISomeComponentOptions> {
...
}

Neste ponto, se você tiver um site de uso dentro do código que será verificado pelo texto datilografado, provavelmente deve apenas morder a bala e confiar nele, mesmo que esteja muito longe.

Se você usar o site externo, convença a comunidade/mantenedores a aceitar o modificador static para membros da interface. Enquanto isso, você precisará de outra solução alternativa. Não está no site de definição, mas acredito que você possa usar uma versão modificada de uma solução alternativa descrita em #8328 para conseguir isso. Veja a solução alternativa no comentário de @mhegazy em 16 de maio de 2016, cortesia de @RyanCavanaugh

Perdoe-me se estiver faltando pontos-chave. Minha compreensão da hesitação em dar suporte a static para membros de interface é principalmente uma aversão ao uso da mesma palavra-chave interface + implements para descrever a forma do construtor e a forma da instância.

Deixe-me começar a seguinte analogia dizendo que eu amo o que o typescript oferece e aprecio muito aqueles que o desenvolveram e a comunidade que coloca muito pensamento e esforço para lidar com a infinidade de solicitações de recursos; fazendo o seu melhor para implementar os recursos que são um bom ajuste.

Mas, a hesitação neste caso me parece um desejo de preservar a 'pureza conceitual' à custa da usabilidade. Mais uma vez, desculpe se esta analogia está fora da base:

Isso lembrou de mudar de Java para C# . Quando vi pela primeira vez o código C# fazendo isso
C# var something = "something"; if(something == "something") { ... }
os alarmes começaram a soar na minha cabeça, o mundo estava acabando, eles precisavam estar usando "something".Equals(something) , etc

== é para referência igual! Você tem um .Equals(...) separado para comparação de strings...
e a string literal precisa vir primeiro para que você não receba uma referência nula chamando .Equals(...) em uma variável nula...
e... e... hiperventilando

E então, depois de algumas semanas usando C# , percebi o quanto era mais fácil usar por causa desse comportamento. Mesmo que isso significasse desistir da distinção clara entre os dois, faz uma melhoria tão dramática na usabilidade que vale a pena.

É assim que me sinto sobre o modificador static para membros da interface. Isso nos permitirá continuar descrevendo a forma da instância como já fazemos, nos permitirá descrever a forma da função construtora, o que só podemos fazer com soluções alternativas ruins caso a caso, e nos permitirá fazer as duas coisas de maneira relativamente limpa e fácil. caminho. Uma grande melhoria na usabilidade, IMO.

Eu vou pesar em abstract static no próximo comentário....

Interfaces sempre requerem reespecificação de tipos de membros em classes de implementação. (a inferência de assinaturas de membros em classes de implementação _explicitamente_ é um recurso separado, ainda não suportado).

O motivo pelo qual você está recebendo erros não está relacionado a esta proposta. Para contornar isso, você precisa usar o modificador readonly na implementação de membros de classe, estáticos ou não, que precisam ser de tipos literais; caso contrário, string será inferido.

Novamente a palavra-chave implements é apenas uma especificação de intenção, ela não influencia a compatibilidade estrutural dos tipos, nem introduz tipagem nominal. Isso não quer dizer que não possa ser útil, mas não altera os tipos.

Então abstract static ... Não vale a pena. Muitos problemas.

Você precisaria de uma sintaxe nova e complexa para declarar o que já será verificado no site de uso implicitamente (se o site de uso for verificado pelo compilador typescript).

Se o site de uso for externo, então o que você realmente precisa são static membros de uma interface... Desculpe terminar de conectar... e boa noite!

@aluanhaddad Verdadeiro, mas mesmo que a inferência de assinaturas de membros em classes de implementação explícita fosse um recurso suportado, não temos maneiras limpas de declarar as assinaturas de membros para o lado estático da classe.

O ponto que eu estava tentando expressar era que não temos maneiras de explicitly declarar a estrutura esperada do lado estático da classe e há casos em que isso importa (ou importará para oferecer suporte a recursos)

Principalmente eu queria refutar esta parte do seu comentário "não importa se a interface (s) que especifica os requisitos para o lado estático da classe é referenciada sintaticamente".

Eu estava tentando usar a inferência de tipo como um exemplo de por que isso importaria com suporte futuro.
Ao referenciar sintaticamente a interface aqui let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' } não precisamos declarar explicitamente o tipo como 'Sim' | 'Não' 100 vezes separadas.

Para obter a mesma inferência de tipo para o lado estático de uma classe (no futuro), precisaremos de alguma maneira de referenciar sintaticamente a(s) interface(s).

Depois de ler seu último comentário, acho que não foi um exemplo tão claro quanto eu esperava. Talvez um exemplo melhor seja quando o site de uso para o lado estático da classe está em um código externo que não será verificado pelo compilador typescript. Nesse caso, atualmente temos que contar com soluções alternativas que essencialmente criam um site de uso artificial, fornecem usabilidade/legibilidade ruim e não funcionam em muitos casos.

À custa de alguma verbosidade, você pode alcançar um nível decente de segurança de tipo:

type HasType<T, Q extends T> = Q;

interface IStatic<X, Y> {
    doWork: (input: X) => Y
}

type A_implments_IStatic<X, Y> = HasType<IStatic<X, Y>, typeof A>    // OK
type A_implments_IStatic2<X> = HasType<IStatic<X, number>, typeof A> // OK
type A_implments_IStatic3<X> = HasType<IStatic<number, X>, typeof A> // OK
class A<X, Y> {
    static doWork<T, U>(_input: T): U {
        return null!;
    }
}

type B_implments_IStatic = HasType<IStatic<number, string>, typeof B> // Error as expected
class B {
    static doWork(n: number) {
        return n + n;
    }
}

Concordo que isso deve ser permitido. O seguinte caso de uso simples se beneficiaria de métodos estáticos abstratos:

export abstract class Component {
  public abstract static READ_FROM(buffer: ByteBuffer): Component;
}

// I want to force my DeckComponent class to implement it's own static ReadFrom method
export class DeckComponent extends Component {
  public cardIds: number[];

  constructor(cardIds: number[]) {
    this.cardIds = cardIds;
  }

  public static READ_FROM(buffer: ByteBuffer): DeckComponent {
    const cardIds: number[] = [...];
    return new DeckComponent(cardIds);
  }
}

@RyanCavanaugh Acho que ninguém mencionou isso exatamente ainda (embora eu possa ter perdido na minha leitura rápida), mas em resposta a:

Uma questão importante que tivemos ao considerar isso: Quem tem permissão para chamar um método estático abstrato? Presumivelmente, você não pode invocar AbstractParentClass.getSomeClassDependentValue diretamente. Mas você pode invocar o método em uma expressão do tipo AbstractParentClass? Se sim, por que isso deveria ser permitido? Se não, qual é o uso do recurso?

Isso pode ser resolvido com a implementação de parte do #3841. Lendo essa questão, a principal objeção levantada parece ser que os tipos de funções construtoras de classes derivadas geralmente não são compatíveis com as funções construtoras de suas classes base. No entanto, a mesma preocupação não parece se aplicar a nenhum outro método ou campo estáticos porque o TypeScript já está verificando se os estáticos substituídos são compatíveis com seus tipos na classe base.

Então, o que eu proponho é dar T.constructor o tipo Function & {{ statics on T }} . Isso permitiria que classes abstratas que declaram um campo abstract static foo o acessassem com segurança via this.constructor.foo sem causar problemas com incompatibilidades de construtor.

E mesmo se a adição automática de estática a T.constructor não for implementada, ainda poderemos usar as propriedades abstract static na classe base declarando manualmente "constructor": typeof AbstractParentClass .

Acho que muitas pessoas esperam que o exemplo @patryk-zielinski93 funcione. Em vez disso, devemos usar soluções alternativas contra-intuitivas, detalhadas e enigmáticas. Como já temos classes 'sintaxe açucaradas' e membros estáticos, por que não podemos ter esse açúcar no sistema de tipos?

Aqui está minha dor:

abstract class Shape {
    className() {
        return (<typeof Shape>this.constructor).className;
    }
    abstract static readonly className: string; // How to achieve it?
}

class Polygon extends Shape {
    static readonly className = 'Polygon';
}

class Triangle extends Polygon {
    static readonly className = 'Triangle';
}

Talvez pudéssemos introduzir/em paralelo uma cláusula static implements ? por exemplo

interface Foo {
    bar(): number;
}

class Baz static implements Foo {
    public static bar() {
        return 4;
    }
}

Isso tem a vantagem de ser compatível com versões anteriores com a declaração de interfaces separadas para membros estáticos e de instância, o que parece ser a solução alternativa atual de escolha, se eu estiver lendo este thread corretamente. É também um recurso sutilmente diferente que eu pude ver sendo útil por si só, mas isso não vem ao caso.

( statically implements seria melhor, mas isso introduz uma nova palavra-chave. Discutível se vale a pena. Mas pode ser contextual.)

Por mais que eu tentasse entender os argumentos dos céticos em relação à proposta do OP, falhei.

Apenas uma pessoa (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) respondeu a você @RyanCavanaugh . Reescrevi o código do OP para torná-lo um pouco mais limpo e também para ilustrar sua pergunta e minhas respostas:

abstract class Base {
  abstract static foo() // ref#1
}

class Child1 extends Base {
  static foo() {...} // ref#2
}

class Child2 extends Base {
  static foo() {...}
}

Quem tem permissão para chamar um método estático abstrato? Presumivelmente, você não pode invocar Base.foo diretamente

Se você quer dizer ref#1, então sim, nunca deve ser chamado, porque bem, é abstrato e nem tem um corpo.
Você só tem permissão para chamar ref#2.

Mas você pode invocar o método em uma expressão do tipo Base?
Se sim, por que isso deveria ser permitido?

function bar(baz:Base) { // "expression of type Base"
  baz.foo() // ref#3
}

function bar2(baz: typeof Base) { // expression of type Base.constructor
  baz.foo() // ref#4
}

ref#3 é um erro. Não, você não pode invocar "foo" lá, porque baz deveria ser uma __instance__ de Child1 ou Child2, e as instâncias não possuem métodos estáticos

ref#4 está (deveria estar) correto. Você pode (deve ser capaz de) invocar o método estático foo lá, porque baz deveria ser o construtor de Child1 ou Child2, que estendem Base e, portanto, devem ter implementado foo().

bar2(Child1) // ok
bar2(Child2) // ok

Você pode imaginar esta situação:

bar2(Base) // ok, but ref#4 should be red-highlighted with compile error e.g. "Cannot call abstract  method." 
// Don't know if it's possible to implement in compiler, though. 
// If not, compiler simply should not raise any error and let JS to do it in runtime (Base.foo is not a function). 

Qual é o uso do recurso?

Veja ref#4 O uso é quando não sabemos qual das classes filhas recebemos como argumento (pode ser Child1 ou Child2), mas queremos chamar seu método estático e ter certeza de que esse método existe.
Estou reescrevendo o framework node.js certo, chamado AdonisJS. Foi escrito em JS puro, então estou transformando para TS. Não posso mudar como o código funciona, estou apenas adicionando tipos. A falta desses recursos me deixa muito triste.

ps Neste comentário, por motivos de simplicidade, escrevi apenas sobre classes abstratas e não mencionei interfaces. Mas tudo o que escrevi é aplicável a interfaces. Você apenas substitui a classe abstrata pela interface e tudo estará correto. Existe a possibilidade, quando a equipe TS por algum motivo (não sei porque) não queira implementar abstract static em abstract class , porém seria bom implementar apenas static palavra em interfaces, isso nos deixaria felizes o suficiente, eu acho.

pps Editei nomes de classes e métodos em suas perguntas para cumpri-los com meu código.

Alguns pensamentos, baseados nos principais argumentos de outros céticos:

"Não, isso não deve ser implementado, mas você já pode fazer assim *escreve 15 vezes mais linhas de código*".

É para isso que o açúcar sintático é feito. Vamos adicionar um pouco (açúcar) ao TS. Mas não, é melhor torturarmos os cérebros dos desenvolvedores que tentam quebrar os decoradores e dezenas de genéricos em vez de adicionar uma simples palavra static às interfaces. Por que não considerar esse static como mais um açúcar para facilitar nossas vidas? Da mesma forma que o ES6 adiciona class (que se torna onipresente hoje em dia). Mas não, vamos ser nerds e fazer as coisas do jeito antigo e "certo".

"Existem duas interfaces para classes js: para construtor e instância".

Ok, por que não nos dar uma maneira de fazer interface para o construtor então? No entanto, simplesmente adicionar essa palavra static é muito mais fácil e intuitivo.

E a respeito disso:

Segurando isso até ouvirmos mais comentários sobre isso.

Mais de um ano se passou e muitos comentários foram fornecidos, por quanto tempo essa questão será adiada? Alguém pode responder, por favor.

Este post pode parecer meio duro... Mas não, é um pedido do cara que ama TS e ao mesmo tempo não consegue encontrar uma razão sólida para irmos com hacks feios, ao converter nossa antiga base de código JS para TS. Além disso, muuuuito obrigado à equipe TS. TS é simplesmente maravilhoso, mudou minha vida e eu gosto mais do que nunca de codificação e meu trabalho... Mas esse problema está envenenando minha experiência.

Solução possível?

export type Constructor<T> = new (...args: any[]) => T;

declare global {
  interface Function extends StaticInterface {
  }
}

export interface StaticInterface {
  builder: (this: Constructor<MyClass>) => MyClass
}

// TODO add decorator that adds "builder" implementation
export class MyClass {
  name = "Ayyy"
}

export class OtherClass {
  id = "Yaaa"
}

MyClass.builder() // ok
OtherClass.builder() // error

Proposta: membros estáticos em interfaces e tipos e membros abstratos em classes abstratas, v2

Casos de uso

Tipos compatíveis com Fantasyland

```` datilografado
interface Aplicativo estende Aplicar {estático de (a: A): Aplicativo ; }

const de = >(c: C, a: A): new C => c.of(a); ````

Tipo $#$ Serializable $#$ com método deserialize estático

```` datilografado
interface serializável {
static desserialize(s: string): Serializable;

serialize(): string;

}
````

Contratos em geral

```` datilografado
contrato de interface {
static create(x: number): Contrato;
static new(x: number): Contrato;
}

const factory1 = (c: Contrato estático): Contrato => c.create(Math.random());
const factory2 = (C: Contrato estático): Contrato => new C(Math.random());
const fabrica3 =(c: C): new C => c.create(Math.random());

const c1 = fabrica1(ContractImpl); // Contract
const c2 = fábrica2(ContractImpl); // Contract
const c3 = fabrica3(ContractImpl); // ContractImpl
````

Sintaxe

Métodos e campos estáticos

```` datilografado
interface serializável {
static desserialize(s: string): Serializable;
}

tipo Serializavel = {
static desserialize(s: string): Serializable;
};

classe abstrata serializável {
static abstract desserialize(s: string): Serializable;
}
````

Assinaturas do construtor estático

typescript interface Contract { static new(): Contract; }

Follow-on: assinaturas de chamada estáticas

Discutir:

Como as assinaturas de chamada estáticas podem ser expressas?
Se vamos expressá-los simplesmente adicionando o modificador static antes da assinatura da chamada,
como vamos distingui-los do método de instância com o nome 'static' ?
Podemos usar a mesma solução alternativa para os métodos 'new' -named?
Nesse caso, será definitivamente uma mudança de ruptura.

O operador de tipo static

typescript const deserialize = (Class: static Serializable, s: string): Serializable => Class.deserialize(s);

O operador de tipo new

typescript const deserialize = <C extends static Serializable>(Class: C, s: string): new C => Class.deserialize(s);

Slots internos

O slot interno [[Static]]

A interface “estática” de um tipo será armazenada no slot interno [[Static]] :

```` datilografado
// O tipo
interface serializável {
static desserialize(s: string): Serializable;
serializar: string;
}

// será representado internamente como
//interface serializável {
// [[Estático]]: {
// [[Instância]]: Serializavel; // Veja abaixo.
// desserialize(s: string): Serializable;
// };
// serialize(): string;
// }
````

Por padrão, digite never .

O slot interno [[Instance]]

A interface “Instância” do tipo será armazenada
no slot interno [[Instance]] #$8$#$ do tipo de slot interno [[Static]] .

Por padrão, digite never .

Semântica

Sem o operador static

Quando um tipo usado como um tipo de valor,
o slot interno [[Static]] será descartado:

```` datilografado
declare const serializável: Serializable;

tipo T = tipo de serializável;
// { serialize(): string; }
````

Mas o tipo em si permanece mantendo o slot interno [[Static]] :

typescript type T = Serializable; // { // [[Static]]: { // [[Instance]]: Serializable; // deserialize(s: string): Serializable; // }; // serialize(): string; // }

Atribuição

Ambos os valores dos tipos com reconhecimento de estática são atribuíveis à estrutura idêntica
(exceto os [[Static]] , claro) e vice-versa.

O operador static

| Associatividade | Precedência |
| :-----------: | :--------------------------------: |
| Direito | IDK, mas igual ao de new |

O operador de tipo static retorna o tipo de slot interno [[Static]] do tipo.
É um pouco semelhante ao operador de tipo typeof ,
mas seu argumento deve ser um tipo em vez de um valor.

typescript type T = static Serializable; // { // [[Instance]]: Serializable; // deserialize(s: string): Serializable; // }

O operador do tipo typeof também descarta o slot interno [[Instance]] :

```` datilografado
declare const SerializableImpl: static Serializable;

tipo T = tipo de SerializableImpl;
// { desserialize(s: string): Serializable; }
````

Atribuição

Ambos os valores dos tipos com reconhecimento de instância são atribuíveis à estrutura idêntica
(exceto o slot interno [[Instance]] , é claro) e vice-versa.

O operador new

| Associatividade | Precedência |
| :-----------: | :-----------------------------------: |
| Direito | IDK, mas igual ao de static |

Os operadores new retornam o tipo de slot interno [[Instance]] do tipo.
Ele efetivamente reverte o operador static :

typescript type T = new static Serializable; // { // [[Static]]: { // [[Instance]]: Serializable; // deserialize(s: string): Serializable; // }; // serialize(): string; // }

extends / implements semântica

Uma classe que implementa uma interface com slot interno [[Static]] não padrão
DEVE ter um slot interno compatível com [[Static]] .
As verificações de compatibilidade devem ser iguais (ou semelhantes)
verificações de compatibilidade regulares (instância).

```` datilografado
class SerializableImpl implementa Serializable {
static desserialize(s: string): SerializableImpl {
// A lógica de desserialização vai aqui.
}

// ...other static members
// constructor
// ...other members

serialize(): string {
    //
}

}
````

FAÇAM

  • [ ] Decida qual sintaxe deve ser usada para assinaturas de chamadas estáticas.
    Possivelmente sem quebrar mudanças.
  • [ ] Existe algum caso especial com tipos condicionais e operador infer ?
  • [ ] Mudanças na semântica de membros de classe não abstratos. _Pode estar quebrando._

Passei algumas horas lendo esse problema e outros problemas para uma solução alternativa para o meu problema, que seria resolvido pelo modificador estático ou interfaces para classes. Não consigo encontrar uma única solução alternativa que resolva meu problema.

O problema que tenho é que quero fazer uma modificação em uma classe gerada por código. Por exemplo, o que eu gostaria de fazer é:

import {Message} from "../protobuf";

declare module "../protobuf" {
    interface Message {
        static factory: (params: MessageParams) => Message
    }
}

Message.factory = function(params: MessageParams) {
    const message = new Message();
    //... set up properties
    return message;
}

export default Message;

Não consigo encontrar uma única solução alternativa que me permita fazer o equivalente a isso, com a versão atual do TS. Estou perdendo uma maneira de resolver este problema atualmente?

Parece relevante postar aqui como um caso de uso para o qual aparentemente não há solução alternativa e certamente nenhuma solução alternativa direta.

Se você quiser verificar a instância e o lado estático de uma classe em relação às interfaces, você pode fazer assim:

interface C1Instance {
  // Instance properties ...

  // Prototype properties ...
}
interface C2Instance extends C1Instance {
  // Instance properties ...

  // Prototype properties ...
}

interface C1Constructor {
  new (): C1Instance;

  // Static properties ...
}
interface C2Constructor {
  new (): C2Instance;

  // Static properties ...
}

type C1 = C1Instance;
let C1: C1Constructor = class {};

type C2 = C2Instance;
let C2: C2Constructor = class extends C1 {};

let c1: C1 = new C1();
let c2: C2 = new C2();

Muitos de nós estão desperdiçando muitas horas do nosso tempo e outros com isso. Por que não é uma coisa?!¿i!
Por que as respostas são todas grandes soluções alternativas para algo que deve ser legível, digerível e algo que é uma linha simples. Não há como eu querer que alguém tenha que descobrir o que meu código está tentando fazer com alguma solução hacky. Desculpe por desordenar ainda mais este tópico com algo que não é muito valioso, mas é uma grande dor e perda de tempo atualmente, então acho valioso para mim, os outros agora e aqueles que vêm mais tarde procurando uma resposta.

A coisa com qualquer linguagem, natural e artificial, que deve evoluir naturalmente para ser eficiente e atraente para ser usada. Eventualmente as pessoas decidiram usar reduções na linguagem, ("okay"=>"ok","going to" =>"gonna"), inventaram novas palavras ridículas, como "selfie" e "google", redefiniram a ortografia com l33tspeak e coisas, e até baniu algumas palavras, e, apesar de você querer usá-las ou não, todo mundo ainda entende o que elas significam, e alguns de nós as usam para realizar algumas tarefas específicas. E para nenhum deles pode haver uma boa razão, mas a eficiência de certas pessoas em certas tarefas, é tudo uma questão de número de pessoas, que realmente fazem uso delas. O volume desta conversa mostra claramente que muitas pessoas poderiam fazer uso deste static abstract para quaisquer considerações que tenham. Eu vim aqui pelo mesmo motivo, porque eu queria implementar Serializable então tentei todas as maneiras intuitivas (para mim) de fazer isso, e nenhuma delas funcionou. Confie em mim, a última coisa que eu procuraria é a explicação de por que não preciso desse recurso e deveria optar por outra coisa. Ano e meio, Jesus Cristo! Aposto que já existe um PR em algum lugar, com testes e tal. Por favor, faça isso acontecer, e se houver uma certa forma de uso que é desencorajada, temos um tslint para isso.

É possível acessar membros estáticos de classes filhas da classe base via this.constructor.staticMember , então membros estáticos abstratos fazem sentido para mim.

class A {
  f() {
    console.log(this.constructor.x)
  }
}

class B extends A {
  static x = "b"
}

const b = new B
b.f() // logs "b"

A classe A deve ser capaz de especificar que requer um membro estático x , porque o usa no método f .

Qualquer notícia ?
Os recursos são realmente necessários 😄
1) static funções em interfaces
2) Funções abstract static em classes abstratas

embora eu odeie a ideia de ter interfaces estáticas, mas para todos os fins práticos, o seguinte deve ser suficiente hoje :

type MyClass =  (new (text: string) => MyInterface) & { myStaticMethod(): string; }

que pode ser usado como:

const MyClass: MyClass = class implements MyInterface {
   constructor(text: string) {}
   static myStaticMethod(): string { return ''; }
}

ATUALIZAR:

mais detalhes sobre a ideia e um exemplo ao vivo:

// dynamic part
interface MyInterface {
    data: number[];
}
// static part
interface MyStaticInterface {
    myStaticMethod(): string;
}

// type of a class that implements both static and dynamic interfaces
type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface;

// way to make sure that given class implements both 
// static and dynamic interface
const MyClass: MyClass = class MyClass implements MyInterface {
   constructor(public data: number[]) {}
   static myStaticMethod(): string { return ''; }
}

// works just like a real class
const myInstance = new MyClass([]); // <-- works!
MyClass.myStaticMethod(); // <-- works!

// example of catching errors: bad static part
/*
type 'typeof MyBadClass1' is not assignable to type 'MyClass'.
  Type 'typeof MyBadClass1' is not assignable to type 'MyStaticInterface'.
    Property 'myStaticMethod' is missing in type 'typeof MyBadClass1'.
*/
const MyBadClass1: MyClass = class implements MyInterface {
   constructor(public data: number[]) {}
   static myNewStaticMethod(): string { return ''; }
}

// example of catching errors: bad dynamic part
/*
Type 'typeof MyBadClass2' is not assignable to type 'MyClass'.
  Type 'typeof MyBadClass2' is not assignable to type 'new (data: number[]) => MyInterface'.
    Type 'MyBadClass2' is not assignable to type 'MyInterface'.
      Property 'data' is missing in type 'MyBadClass2'.
*/
const MyBadClass2: MyClass = class implements MyInterface {
   constructor(public values: number[]) {}
   static myStaticMethod(): string { return ''; }
}

@aleksey-bykov isso pode não ser culpa do Typescript, mas não consegui esses decoradores de componentes Angular funcionando e seu compilador AoT.

@aleksey-bykov isso é inteligente, mas ainda não funciona para estática abstrata. Se você tiver quaisquer subclasses de MyClass , elas não serão aplicadas com verificação de tipo. Também é pior se você tiver genéricos envolvidos.

// no errors
class Thing extends MyClass {

}

Eu realmente espero que a equipe do TypeScript reconsidere sua posição sobre isso, porque construir bibliotecas de usuário final que requerem atributos estáticos não tem nenhuma implementação razoável. Devemos ser capazes de ter um contrato que exija que implementadores de interface/extensores de classe abstrata tenham estática.

@bbugh eu questiono a própria existência do problema que está sendo discutido aqui, por que você precisaria de todos esses problemas com métodos herdados abstratos estáticos se o mesmo pode ser feito por meio de instâncias de classes regulares?

class MyAbstractStaticClass {
    abstract static myStaticMethod(): void; // <-- wish we could
}
class MyStaticClass extends MyAbstractStaticClass {
    static myStaticMethod(): void {
         console.log('hi');
    }
}
MyStaticClass.myStaticMethod(); // <-- would be great

vs

class MyAbstractNonStaticClass {
    abstract myAbstractNonStaticMethod(): void;
}
class MyNonStaticClass extends MyAbstractNonStaticClass {
    myNonStaticMethod(): void {
        console.log('hi again');
    }
}
new MyNonStaticClass().myNonStaticMethod(); // <-- works today

@aleksey-bykov Existem muitas razões. Por exemplo ( de @patryk-zielinski93):

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

Eu quero forçar a implementação do método deserialize estático nas subclasses de Serializable.
Existe alguma solução alternativa para implementar esse comportamento?

EDIT: Eu sei que você também pode usar deserialize como construtor, mas as classes só podem ter 1 construtor, o que torna os métodos de fábrica necessários. As pessoas querem uma maneira de exigir métodos de fábrica nas interfaces, o que faz todo o sentido.

simplesmente leve a lógica de deserialização para uma classe separada, porque não há benefício em ter um método de desserialização estático anexado à própria classe que está sendo desserializada

class Deserializer {
     deserializeThis(...): Xxx {}
     deserializeThat(...): Yyy {}
}

qual é o problema?

@aleksey-bykov aula separada não parece tão bonita

@aleksey-bykov, o problema é que há mais de 1 classe exigindo serialização, então sua abordagem está forçando a criação de um dicionário de serializáveis, que é um antipadrão uberclass, e cada modificação em qualquer um deles exigiria uma edição nesta uberclass, o que torna o suporte de código um incômodo. Embora ter uma interface serializável possa forçar uma implementação para qualquer tipo de objeto, e também suportar herança polimórfica, que são as principais razões pelas quais as pessoas a desejam. Por favor, não especule se há um benefício ou não, qualquer um deve poder ter opções e escolher o que é benéfico para seu próprio projeto.

@octaharon ter uma aula exclusivamente dedicada à desserialização é exatamente o oposto do que você disse, tem responsabilidade única porque a única vez que você muda é quando se preocupa com a desserialização

ao contrário, adicionar desserialização à própria classe, como você propôs, dá a ela uma responsabilidade adicional e um motivo adicional para ser alterada

por último, não tenho problemas com métodos estáticos, mas estou realmente curioso para ver um exemplo de um caso de uso prático para métodos estáticos abstratos e interfaces estáticas

@octaharon ter uma aula exclusivamente dedicada à desserialização é exatamente o oposto do que você disse, tem responsabilidade única porque a única vez que você muda é quando se preocupa com a desserialização

exceto que você precisa alterar o código em dois lugares em vez de um, pois sua (des)serialização depende da sua estrutura de tipo. Isso não conta para "responsabilidade única"

ao contrário, adicionar desserialização à própria classe, como você propôs, dá a ela uma responsabilidade adicional e um motivo adicional para ser alterada

Eu não entendo o que você está dizendo. Uma classe que seja responsável por sua própria (des)serialização é exatamente o que eu quero, e por favor, não me diga se está certo ou errado.

responsabilidade única não diz nada sobre quantos arquivos estão envolvidos, apenas diz que deve haver um único motivo

o que estou dizendo é que quando você adiciona uma nova classe você quer dizer algo diferente de apenas ser desserializado, então ela já tem uma razão de existir e responsabilidade atribuída a ela; então você adiciona outra responsabilidade de poder desserializar a si mesmo, isso nos dá 2 responsabilidades, isso viola o SOLID, se você continuar adicionando mais coisas como renderizar, imprimir, copiar, transferir, criptografar, etc, você obterá uma classe deus

e perdoe-me dizer banalidades, mas perguntas (e respostas) sobre benefícios e casos de uso práticos é o que impulsiona esta proposta de recurso a ser implementada

SOLID não foi enviado por Deus Todo-Poderoso em um tablet e, mesmo que fosse, há um grau de liberdade em sua interpretação. Você pode ser tão idealista quanto desejar, mas faça-me um favor: não espalhe suas crenças pela comunidade. Todo mundo tem todo o direito de usar qualquer ferramenta da maneira que quiser e quebrar todas as regras possíveis que conhece (você não pode culpar uma faca por um assassinato). O que define a qualidade de uma ferramenta é um equilíbrio entre a demanda por determinados recursos e a oferta deles. E esse ramo mostra o volume da demanda. Se você não precisa desse recurso - não o use. Eu faço. E um monte de gente aqui precisa disso também, e _nós_ temos um caso de uso prático, enquanto você está dizendo que deveríamos desconsiderá-lo, na verdade. É apenas sobre o que é mais importante para os mantenedores - os princípios sagrados (de qualquer forma que eles entendam), ou a comunidade.

cara, qualquer boa proposta apresenta casos de uso, esta não

image

então eu apenas expressei alguma curiosidade e fiz uma pergunta, já que a proposta não diz, por que vocês podem precisar

tudo se resumia ao típico: justa causa, não se atreva

pessoalmente, eu não me importo com sólido ou oop, eu acabei de crescer muito tempo atrás, você trouxe isso jogando o argumento do "antipadrão uberclass" e depois fez o backup para "um grau de liberdade para sua interpretação"

a única razão prática mencionada em toda essa discussão é esta: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119

Um caso de uso muito comum com o qual isso ajudará são os componentes React, que são classes com propriedades estáticas, como displayName, propTypes e defaultProps.

e algumas postagens semelhantes https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -345496014

mas está coberto por (new (...) => MyClass) & MyStaticInterface

esse é o caso de uso exato e uma razão pela qual estou aqui. Você vê o número de votos? Por que você acha que cabe a você decidir pessoalmente o que é practical e o que não é? Practical é o que pode ser colocado em practice , e (no momento em que escrevo) 83 pessoas achariam esse recurso muito prático. Por favor, respeite os outros e leia o tópico completo antes de começar a tirar as frases de um contexto e exibir várias palavras-chave. O que quer que você tenha superado, isso definitivamente não é o seu ego.

é senso comum que as coisas práticas são as que resolvem os problemas, as não práticas são as que aguçam o seu senso de beleza, eu respeito os outros mas com todo esse respeito a pergunta (principalmente retórica agora) ainda vale: que problema esta proposta pretende resolva dado (new (...) => MyClass) & MyStaticInterface para https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119 e https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -289084844 justa causa

por favor não responda

Pela mesma razão que às vezes uso declarações type para reduzir anotações grandes, acho que uma palavra-chave como abstract static seria muito mais legível do que uma construção relativamente mais difícil de digerir, como foi mencionada como existente exemplo.

Além disso, ainda não abordamos classes abstratas?

A solução para não usar classes abstratas não é a solução, na minha opinião. Isso é uma solução! Uma solução em torno de quê?

Acho que essa solicitação de recurso existe porque muitas pessoas, incluindo o solicitante, descobriram que um recurso esperado, como abstract static ou static nas interfaces, não estava presente.

Com base na solução oferecida, a palavra-chave static precisa existir se houver uma solução alternativa para evitar seu uso? Acho que seria igualmente ridículo sugerir.

O problema aqui é que static faz muito mais sentido.
Com base no interesse gerado, podemos ter uma discussão menos desdenhosa?

Houve alguma atualização sobre esta proposta? Algum argumento que valha a pena considerar que demonstre por que não devemos ter static abstract e coisas do gênero?
Podemos ter mais sugestões que mostrem por que seria útil?

Talvez precisemos resumir as coisas e resumir o que foi discutido para que possamos encontrar uma solução.

Existem duas propostas, pelo que entendo:

  1. interfaces e tipos podem definir propriedades e métodos estáticos
interface ISerializable<T> { 
   static fromJson(json: string): T;
}
  1. classes abstratas podem definir métodos estáticos abstratos
abstract class MyClass<T> implements ISerializable<T> {
   abstract static fromJson(json: string): T;
}

class MyOtherClass extends MyClass<any> {
  static fromJson(json: string) {
  // unique implementation
  }
}

Quanto à proposta um, há tecnicamente uma solução alternativa! O que não é ótimo, mas isso é algo pelo menos. Mas é uma solução alternativa.

Você pode dividir suas interfaces em duas e reescrever sua classe como

interface StaticInterface {
  new(...args) => MyClass;
  fromJson(json): MyClass;
}

interface InstanceInterface {
  toJson(): string;
}

const MyClass: StaticInterface = class implements InstanceInterface {
   ...
}

Na minha opinião, isso é muito trabalho extra e um pouco menos legível, e tem a desvantagem de reescrever suas classes de uma maneira engraçada que é simplesmente estranha e desvia da sintaxe que estamos usando.

Mas então, e a proposta 2? Não há nada que possa ser feito sobre isso, não é? Acho que isso merece ser abordado também!

Qual é o uso prático para um desses tipos - como um deles seria usado?

interface JsonSerializable {
    toJSON(): string;
    static fromJSON(serializedValue: string): JsonSerializable;
}

Já é possível dizer que um valor deve ser um objeto como { fromJSON(serializedValue: string): JsonSerializable; } , então isso é apenas desejado para impor um padrão? Eu não vejo o benefício disso de uma perspectiva de verificação de tipo. Como uma observação lateral: neste caso, seria impor um padrão com o qual é difícil trabalhar - seria melhor mover o processo de serialização para classes ou funções de serializador separadas por muitos motivos que não entrarei aqui.

Além disso, por que algo assim está sendo feito?

class FirstChildClass extends AbstractParentClass {
    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

Que tal usar um método de modelo ou padrão de estratégia? Isso funcionaria e seria mais flexível, certo?

No momento, sou contra esse recurso porque, para mim, parece adicionar complexidade desnecessária para descrever designs de classe com os quais é difícil trabalhar. Talvez haja algum benefício que estou perdendo?

existe um caso de uso válido para métodos React estáticos, é isso

@aleksey-bykov ah, ok. Para esses casos, pode ser melhor se adicionar um tipo na propriedade constructor causar a verificação de tipo nesse cenário raro. Por exemplo:

interface Component {
    constructor: ComponentConstructor;
}

interface ComponentConstructor {
    displayName?: string;
}

class MyComponent implements Component {
    static displayName = 5; // error
}

Isso me parece muito mais valioso. Ele não adiciona nenhuma complexidade adicional à linguagem e apenas adiciona mais trabalho para o verificador de tipos ao verificar se uma classe implementa uma interface corretamente.


A propósito, eu estava pensando que um caso de uso válido seria ao viajar de uma instância para o construtor e, finalmente, para um método ou propriedade estática com verificação de tipo, mas isso já é possível digitando a propriedade constructor em um tipo e será resolvido para instâncias de classe em #3841.

O padrão de serialização/desserialização descrito upthread não é "válido"?

@dsherret não "vê o benefício de uma perspectiva de verificação de tipo". O objetivo principal de fazer análise estática é detectar erros o mais cedo possível. Eu digito coisas para que, se uma assinatura de chamada precisar ser alterada, todos que a chamarem - ou, criticamente, todos os responsáveis ​​pela implementação de métodos que usam a assinatura - atualizarão para a nova assinatura.

Suponha que eu tenha uma biblioteca que forneça um conjunto de classes irmãs com um método estático foo(x: number, y: boolean, z: string) , e a expectativa é que os usuários escrevam uma classe de fábrica que receba várias dessas classes e chame o método para construir instâncias. (Talvez seja deserialize ou clone ou unpack ou loadFromServer , não importa.) O usuário também cria subclasses do mesmo pai (possivelmente abstrato) classe da biblioteca.

Agora, preciso alterar esse contrato para que o último parâmetro seja um objeto de opções. Passar um valor de string para o terceiro argumento é um erro irrecuperável que deve ser sinalizado em tempo de compilação. Qualquer classe de fábrica deve ser atualizada para passar um objeto ao chamar foo , e as subclasses que implementam foo devem alterar sua assinatura de chamada.

Quero garantir que os usuários que atualizarem para a nova versão da biblioteca captarão as alterações importantes em tempo de compilação. A biblioteca precisa exportar uma das soluções alternativas de interface estática acima (como type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface; ) e esperar que o consumidor a aplique em todos os lugares certos. Se eles esqueceram de decorar uma de suas implementações ou se não usaram um tipo de biblioteca exportada para descrever a assinatura de chamada de foo em sua classe de fábrica, o compilador não pode dizer nada alterado e eles recebem erros de tempo de execução . Compare isso com uma implementação sensata de métodos abstract static na classe pai -- sem necessidade de anotações especiais, sem carga no código de consumo, apenas funciona fora da caixa.

@thw0rted a maioria das bibliotecas não exigirá algum tipo de registro dessas classes e a verificação de tipos pode ocorrer nesse ponto? Por exemplo:

// in the library code...
class SomeLibraryContext {
    register(classCtor: Function & { deserialize(serializedString: string): Component; }) {
        // etc...
    }
}

// then in the user's code
class MyComponent extends Comonent {
    static deserialize(serializedString: string) {
        return JSON.parse(serializedString) as Component;
    }
}

const context = new SomeLibraryContext();
// type checking occurs here today. This ensures `MyComponent` has a static deserialize method
context.register(MyComponent);

Ou as instâncias dessas classes estão sendo usadas com a biblioteca e a biblioteca vai da instância ao construtor para obter os métodos estáticos? Nesse caso, é possível alterar o design da biblioteca para não exigir métodos estáticos.

Como uma nota lateral, como um implementador de uma interface, eu ficaria muito irritado se fosse forçado a escrever métodos estáticos porque é muito difícil injetar dependências em um método estático devido à falta de um construtor (nenhuma dependência ctor é possível). Também dificulta a troca da implementação da desserialização por outra coisa, dependendo da situação. Por exemplo, digamos que eu estava desserializando de diferentes fontes com diferentes mecanismos de serialização... agora eu tenho um método de desserialização estático que, por design, só pode ser implementado de uma maneira para uma implementação de uma classe. Para contornar isso, eu precisaria ter outra propriedade ou método estático global na classe para dizer ao método deserialize estático o que usar. Eu preferiria uma interface Serializer separada que permitiria trocar facilmente o que usar e não acoplar a (des)serialização à classe que está sendo (des)serializada.

Entendo o que você quer dizer -- para usar um padrão de fábrica, eventualmente você precisa passar a classe de implementação para a fábrica, e a verificação estática ocorre nesse momento. Eu ainda acho que pode haver momentos em que você deseja fornecer uma classe compatível com a fábrica, mas não a usa no momento; nesse caso, uma restrição abstract static detectaria problemas mais cedo e tornaria o significado mais claro.

Eu tenho outro exemplo que acho que não entra em conflito com sua preocupação com a "nota lateral". Eu tenho várias classes irmãs em um projeto de frontend, onde dou ao usuário uma escolha de qual "provedor" usar para um determinado recurso. Claro, eu poderia fazer um dicionário em algum lugar de name => Provider e usar as chaves para determinar o que mostrar na lista de opções, mas a maneira como implementei agora é exigir um campo estático name em cada implementação do Provedor.

Em algum momento, mudei isso para exigir shortName e longName (para exibição em diferentes contextos com diferentes quantidades de espaço de tela disponível). Teria sido muito mais simples alterar abstract static name: string; para abstract static shortName: string; etc, em vez de alterar o componente da lista de opções para ter um providerList: Type<Provider> & { shortName: string } & { longName: string } . Ele transmite a intenção, no lugar certo (ou seja, no pai abstrato, não no componente de consumo) e é fácil de ler e alterar. Acho que podemos dizer que há uma solução alternativa, mas ainda acredito que seja objetivamente pior do que as mudanças propostas.

Recentemente, me deparei com esse problema, quando precisei usar o método estático em uma interface. Peço desculpas, se isso já foi abordado antes, pois não tenho tempo para ler essa quantidade de comentários.

Meu problema atual: tenho uma biblioteca .js privada, que quero usar no meu projeto TypeScript. Então eu fui em frente e comecei a escrever um arquivo .d.ts para aquela biblioteca, mas como a biblioteca usa métodos estáticos, eu não consegui terminar isso. Qual é a abordagem sugerida neste caso?

Obrigado por respostas.

@greeny aqui está uma solução de trabalho: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

especificamente esta parte:

type MyClass = (new (text: string) => MyInterface) & { myStaticMethod(): string; }

Outro caso de uso: código gerado / correção parcial de classe

  • Estou usando a biblioteca @rsuter/nswag, que gera especificações de swagger.
  • Você pode escrever um arquivo de 'extensões' que é mesclado no arquivo gerado.
  • O arquivo de extensões nunca é executado, mas precisa compilar sozinho!
  • Às vezes, dentro desse arquivo, preciso me referir a algo que ainda não existe (porque é gerado)
  • Portanto, quero declarar um shim/interface para ele da seguinte maneira
  • Nota: Você pode especificar que certas instruções import sejam ignoradas na mesclagem final para que a inclusão 'shim' seja ignorada no código gerado final.
interface SwaggerException
{
    static isSwaggerException(obj: any): obj is SwaggerException;
}

Mas eu não posso fazer isso e tenho que realmente escrever uma classe - o que é bom, mas ainda parece errado. Eu só quero - como muitos outros disseram - dizer 'este é o contrato'

Só queria adicionar isso aqui, já que não vejo outras menções à geração de código - mas muitos comentários 'por que você precisaria disso' que ficam irritantes. Eu esperaria que uma porcentagem decente de pessoas 'precisando' desse recurso 'estático' em uma interface estivesse fazendo isso apenas para que pudessem se referir a itens de bibliotecas externas.

Também gostaria de ver arquivos d.ts mais limpos - deve haver alguma sobreposição com esse recurso e esses arquivos. É difícil entender algo como JQueryStatic porque parece que é apenas um hack. Além disso, a realidade é que os arquivos d.ts geralmente estão desatualizados e não são mantidos e você mesmo precisa declarar os calços.

(desculpe por mencionar jQuery)

Para o caso de serialização eu fiz algo assim.

export abstract class World {

    protected constructor(json?: object) {
        if (json) {
            this.parseJson(json);
        }
    }

    /**
     * Apply data from a plain object to world. For example from a network request.
     * <strong i="6">@param</strong> json Parsed json object
     */
    abstract parseJson(json: object): void;

    /**
     * Parse instance to plane object. For example to send it through network.
     */
    abstract toJson(): object;
}

Mas ainda seria muito mais fácil usar algo assim:

export abstract class World {

    /**
     * Create a world from plain object. For example from a network request.
     * <strong i="10">@param</strong> json Parsed json object
     */
    abstract static fromJson(json: object): World;

    /**
     * Parse instance to plane object. For example to send it through network.
     */
    abstract toJson(): object;
}

Li muito esse tópico e ainda não entendo por que é bom e correto dizer não a esse padrão. Se você compartilha dessa opinião, você também diz que java e outras linguagens populares fazem isso errado. Ou eu estou errado?

Esta edição está aberta há dois anos e tem 79 comentários. Ele é rotulado como Awaiting More Feedback . Você poderia dizer o que mais feedback você precisa para tomar uma decisão?

é realmente irritante que eu não possa descrever o método estático nem na interface nem na classe abstrata (somente declaração). É tão feio escrever soluções alternativas porque esse recurso ainda não foi implementado =(

Por exemplo, next.js usa uma função estática getInitialProps para obter as propriedades da página antes de construir a página. Caso seja lançada, a página não é construída, mas sim a página de erro.

https://github.com/zeit/next.js/blob/master/packages/next/README.md#fetching -data-and-component-lifecycle

Mas, infelizmente, pistas que implementam esse método podem fornecer qualquer assinatura de tipo, mesmo que causem erros em tempo de execução, porque não podem ser verificados de tipo.

Eu acho que esse problema existe aqui há tanto tempo é porque o próprio JavaScript não é bom em coisas estáticas 🤔

A herança estática nunca deve existir em primeiro lugar. 🤔🤔

Quem quer chamar um método estático ou ler um campo estático? 🤔🤔🤔

  • classe filha: não deve ser estática
  • classe pai: use o argumento do construtor
  • outro: use a interface ISomeClassConstructor

Existe algum outro caso de uso?🤔🤔🤔🤔

Alguma atualização sobre isso?

Se for de alguma ajuda, o que eu fiz nos casos em que preciso digitar a interface estática para uma classe é usar um decorador para impor os membros estáticos na classe

O decorador é definido como:

export const statics = <T extends new (...args: Array<unknown>) => void>(): ((c: T) => void) => (_ctor: T): void => {};

Se eu tiver a interface de membro do construtor estático definida como

interface MyStaticType {
  new (urn: string): MyAbstractClass;
  isMember: boolean;
}

e invocado na classe que deve declarar estaticamente os membros em T como:

@statics<MyStaticType>()
class MyClassWithStaticMembers extends MyAbstractClass {
  static isMember: boolean = true;
  // ...
}

O exemplo mais frequente é bom:

interface JsonSerializable {
    toJSON(): string;
    static fromJSON(serializedValue: string): JsonSerializable;
}

Mas como dito em #13462 aqui :

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.

Concordo com o ponto de que as interfaces, no TypeScript, descrevem apenas uma instância de objeto e como usá-la. O problema é que uma instância de objeto não é uma definição de classe , e um símbolo static pode existir apenas em uma definição de classe .

Assim, posso propor o seguinte, com todas as suas falhas:


Uma interface pode estar descrevendo um objeto ou uma classe . Digamos que uma interface de classe seja marcada com as palavras-chave class_interface .

class_interface ISerDes {
    serialize(): string;
    static deserialize(str: string): ISerDes
}

As classes (e interfaces de classe) podem usar as palavras-chave statically implements para declarar seus símbolos estáticos usando uma interface de objeto (as interfaces de classe não podem ser statically implementadas).

As classes (e interfaces de classe) ainda usariam a palavra-chave implements com uma interface de objeto ou uma interface de classe .

Uma interface de classe poderia, então, a mistura entre uma interface de objeto implementada estaticamente e uma interface implementada por instância. Assim, poderíamos obter o seguinte:

interface ISerializable{
    serialize(): string;
}
interface IDeserializable{
    deserialize(str: string): ISerializable
}

class_interface ISerDes implements ISerializable statically implements IDeserializable {}

Dessa forma, as interfaces poderiam manter seu significado, e class_interface seria um novo tipo de símbolo de abstração dedicado a definições de classes.

Um pequeno caso de uso não crítico a mais:
Em Angular para compilação AOT, você não pode chamar funções em decoradores (como no decorador de módulo @NgModule )
problema angular

Para o módulo do service worker, você precisa de algo assim:
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
Nosso ambiente usando subclasses, estendendo a classe abstrata com valores padrão e propriedades abstratas para implementar. Exemplo de implementação semelhante
Portanto, o AOT não funciona porque construir uma classe é uma função e gera erros como: Function calls are not supported in decorators but ..

Para fazê-lo funcionar e manter o suporte ao autocompletion/compiler, é possível definir as mesmas propriedades em nível estático. Mas para 'implementar' propriedades, precisamos de interface com membros estáticos ou membros estáticos abstratos na classe abstrata. Ambos ainda não são possíveis.

com interface poderia funcionar assim:

// default.env.ts
interface ImplementThis {
  static propToImplement: boolean;
}

class DefaultEnv {
  public static production: boolean = false;
}

// my.env.ts
class Env extends DefaultEnv implements ImplementThis {
  public static propToImplement: true;
}

export const environment = Env;

com estática abstrata poderia funcionar assim:

// default.env.ts
export abstract class AbstractDefaultEnv {
  public static production: boolean = false;
  public abstract static propToImplement: boolean;
}
// my.env.ts
class Env extends AbstractDefaultEnv {
  public static propToImplement: true;
}

export const environment = Env;

existem soluções alternativas , mas todas elas são fracas :/

@DanielRosenwasser @RyanCavanaugh pede desculpas pelas menções, mas parece que essa sugestão de recurso - que tem muito apoio da comunidade e acho que seria bastante fácil de implementar - ficou profundamente enterrada na categoria Problemas. Algum de vocês tem algum comentário sobre esse recurso, e um PR seria bem-vindo?

Este problema não é uma duplicata do nº 1263? 😛

26398 (tipo verificar membros estáticos com base na propriedade de construtor do tipo implementa) parece uma solução melhor... se algo assim fosse implementado, então eu espero que seja esse. Isso não requer nenhuma sintaxe/análise adicional e é apenas uma alteração de verificação de tipo para um único cenário. Também não levanta tantas questões como esta.

Eu sinto que métodos estáticos em interfaces não são tão intuitivos quanto ter métodos abstratos estáticos em classes abstratas.

Eu acho que é um pouco esboçado adicionar métodos estáticos a interfaces porque uma interface deve definir um objeto, não uma classe. Por outro lado, uma classe abstrata certamente deve ter permissão para ter métodos abstratos estáticos, uma vez que classes abstratas são usadas para definir subclasses. No que diz respeito à implementação disso, ela simplesmente precisaria ser verificada ao estender a classe abstrata (por exemplo class extends MyAbstractClass ), não ao usá-la como um tipo (por exemplo let myInstance: MyAbstractClass ).

Exemplo:

abstract class MyAbstractClass {
  static abstract bar(): number;
}

class Foo extends MyAbstractClass {
  static bar() {
    return 42;
  }
}

agora por necessidade eu uso isso

abstract class MultiWalletInterface {

  static getInstance() {} // can't write a return type MultiWalletInterface

  static deleteInstance() {}

  /**
   * Returns new random  12 words mnemonic seed phrase
   */
  static generateMnemonic(): string {
    return generateMnemonic();
  }
}

isso é inconveniente!

Eu vim com um problema em que estou adicionando propriedades ao "Objeto", aqui está um exemplo de sandbox

interface Object {
    getInstanceId: (object: any) => number;
}

Object.getInstanceId = () => 42;
const someObject = {};
Object.getInstanceId(someObject); // correct
someObject.getInstanceId({}); // should raise an error but does not

Qualquer instância de objeto agora é considerada como tendo a propriedade getInstanceId enquanto apenas Object deveria. Com uma propriedade estática, o problema teria sido resolvido.

Você deseja aumentar ObjectConstructor, não Object. Você está declarando um método de instância, quando na verdade deseja anexar um método ao próprio construtor. Eu acho que isso já é possível através da mesclagem de declarações:

````ts
declarar global {
interface ObjectConstructor {
ola(): string;
}
}

Object.hello();
````

@thw0rted Excelente! obrigado eu não estava ciente do ObjectConstructor

O ponto maior é que você está aumentando o tipo de construtor em vez do tipo de instância. Acabei de procurar a declaração Object em lib.es5.d.ts e descobri que é do tipo ObjectConstructor .

É difícil para mim acreditar que esse problema ainda está por aí. Este é um recurso legitimamente útil, com vários casos de uso reais.

O ponto principal do TypeScript é ser capaz de garantir a segurança de tipo em nossa base de código, então, por que esse recurso ainda está "pendente de feedback" após dois anos de feedback?

Eu posso estar longe disso, mas algo como as metaclasses do Python seriam capazes de resolver esse problema de uma maneira nativa e sancionada (ou seja, não um hack ou solução alternativa) sem violar o paradigma do TypeScript (ou seja, manter tipos de TypeScript separados para a instância e a classe)?

Algo assim:

interface DeserializingClass<T> {
    fromJson(serializedValue: string): T;
}

interface Serializable {
    toJson(): string;
}

class Foo implements Serializable metaclass DeserializingClass<Foo> {
    static fromJson(serializedValue: string): Foo {
        // ...
    }

    toJson(): string {
        // ...
    }
}

// And an example of how this might be used:
function saveObject(Serializable obj): void {
    const serialized: string = obj.toJson();
    writeToDatabase(serialized);
}

function retrieveObject<T metaclass DeserializingClass<T>>(): T {
    const serialized: string = getFromDatabase();
    return T.fromJson(serialized);
}

const foo: Foo = new Foo();
saveObject(foo);

const bar: Foo = retrieveObject<Foo>();

Honestamente, a parte mais complicada dessa abordagem parece que seria criar uma palavra-chave significativa do TypeScript para metaclass ... staticimplements , classimplements , withstatic , implementsstatic ... não tenho certeza.

Isso é um pouco como a proposta do @GerkinDev , mas sem o tipo separado de interface. Aqui, há um único conceito de interface, e eles podem ser usados ​​para descrever a forma de uma instância ou de uma classe. Palavras-chave na definição da classe de implementação informariam ao compilador em que lado cada interface deve ser verificada.

Vamos retomar a discussão em #34516 e #33892, dependendo de qual recurso você está procurando

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