Typescript: Sugerencia: agregue métodos estáticos abstractos en clases y métodos estáticos en interfaces

Creado en 12 mar. 2017  ·  98Comentarios  ·  Fuente: microsoft/TypeScript

Como continuación del problema #2947, que permite el modificador abstract en declaraciones de métodos pero no lo permite en declaraciones de métodos estáticos, sugiero expandir esta funcionalidad a declaraciones de métodos estáticos permitiendo el modificador abstract static en declaraciones de métodos .

El problema relacionado se refiere al modificador static en la declaración de métodos de interfaz, que no está permitido.

1. El problema

1.1. Métodos estáticos abstractos en clases abstractas

En algunos casos de uso de la clase abstracta y sus implementaciones, es posible que necesite tener algunos valores dependientes de la clase (no dependientes de la instancia), a los que se debe acceder dentro del contexto de la clase secundaria (no dentro del contexto de un objeto), sin creando un objeto. La característica que permite hacer esto es el modificador static en la declaración del método.

Por ejemplo (ejemplo 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'

Pero en algunos casos también necesito acceder a este valor cuando solo sé que la clase de acceso se hereda de AbstractParentClass, pero no sé a qué clase secundaria específica estoy accediendo. Así que quiero estar seguro de que todos los elementos secundarios de AbstractParentClass tienen este método estático.

Por ejemplo (ejemplo 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, el compilador decide que ocurrió un error y muestra el mensaje: La propiedad 'getSomeClassDependentValue' no existe en el tipo 'typeof AbstractParentClass'.

1.2. Métodos estáticos en interfaces

En algunos casos, la lógica de la interfaz implica que las clases de implementación deben tener un método estático, que tiene la lista predeterminada de parámetros y devuelve el valor del tipo exacto.

Por ejemplo (ejemplo 3):

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

Al compilar este código, se produce un error: el modificador 'estático' no puede aparecer en un miembro de tipo.

2. La solución

La solución a ambos problemas (1.1 y 1.2) es permitir el modificador abstract en declaraciones de métodos estáticos en clases abstractas y el modificador static en interfaces.

3. Implementación de JS

La implementación de esta función en JavaScript debe ser similar a la implementación de interfaces, métodos abstractos y métodos estáticos.

Esto significa que:

  1. La declaración de métodos estáticos abstractos en una clase abstracta no debería afectar la representación de la clase abstracta en el código JavaScript.
  2. Declarar métodos estáticos en la interfaz no debería afectar la representación de la interfaz en código JavaScript (no está presente).

Por ejemplo, este código TypeScript (ejemplo 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';
    }
}

debe ser compilado a 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. Puntos relevantes

  • La declaración de un método estático abstracto de una clase abstracta debe marcarse con el modificador abstract static
  • La implementación del método estático abstracto de una clase abstracta debe marcarse con el modificador abstract static o static
  • La declaración del método estático de una interfaz debe marcarse con el modificador static
  • La implementación del método estático de una interfaz debe marcarse con el modificador static

Todas las demás propiedades del modificador abstract static se deben heredar de las propiedades de los modificadores abstract y static .

Todas las demás propiedades del modificador de método de interfaz static deben heredarse de los métodos de interfaz y las propiedades del modificador static .

5. Lista de verificación de características del idioma

  • Sintáctico

    • _¿Cuál es la gramática de esta característica?_ - La gramática de esta característica es abstract static modificador de un método de clase abstracta y static modificador de un método de interfaz.

    • _¿Hay alguna implicación para la retrocompatibilidad de JavaScript? Si es así, ¿están suficientemente mitigados?_ - No hay ninguna implicación para la retrocompatibilidad de JavaScript.

    • _¿Esta sintaxis interfiere con ES6 o cambios plausibles de ES7?_ - Esta sintaxis no interfiere con ES6 o cambios plausibles de ES7.

  • Semántico

    • _¿Qué es un error en la función propuesta?_ - Ahora hay errores al compilar el modificador abstract static de un método de clase abstracto y el modificador static de un método de interfaz.

    • _¿Cómo afecta la característica a las relaciones de subtipo, supertipo, identidad y asignabilidad?_ - La característica no afecta a las relaciones de subtipo, supertipo, identidad y asignabilidad.

    • _¿Cómo interactúa la función con los genéricos?_ - La función no interactúa con los genéricos.

  • Emitir

    • _¿Cuáles son los efectos de esta función en la emisión de JavaScript?_ - No hay efectos de esta función en la emisión de JavaScript.

    • _¿Esto emite correctamente en presencia de variables de tipo 'cualquiera'?_ - Si.

    • _¿Cuáles son los impactos que se emiten en el archivo de declaración (.d.ts)?_ - No hay impactos en el archivo de declaración.

    • _¿Esta característica funciona bien con módulos externos?_ - Sí.

  • Compatibilidad

    • _¿Es este un cambio radical con respecto al compilador 1.0?_ - Probablemente sí, el compilador 1.0 no podrá compilar el código que implementa esta función.

    • _¿Es este un cambio importante en el comportamiento de JavaScript?_ - No.

    • _¿Es esta una implementación incompatible de una característica futura de JavaScript (es decir, ES6/ES7/posterior)?_ - No.

  • Otro

    • _¿Se puede implementar la característica sin afectar negativamente el rendimiento del compilador?_ - Probablemente sí.

    • _¿Qué impacto tiene en los escenarios de herramientas, como la finalización de miembros y la ayuda de firma en los editores?_ - Probablemente no tenga ningún impacto de este tipo.

Awaiting More Feedback Suggestion

Comentario más útil

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

Quiero forzar la implementación del método de deserialización estática en las subclases de Serializable.
¿Hay alguna solución para implementar tal comportamiento?

Todos 98 comentarios

Los métodos de interfaz estática generalmente no tienen sentido, consulte #13462

Aplazando esto hasta que escuchemos más comentarios al respecto.

Una pregunta importante que tuvimos al considerar esto: ¿Quién puede llamar a un método abstract static ? Presumiblemente, no puede invocar AbstractParentClass.getSomeClassDependentValue directamente. Pero, ¿puede invocar el método en una expresión de tipo AbstractParentClass ? Si es así, ¿por qué debería permitirse eso? Si no, ¿cuál es el uso de la función?

Los métodos de interfaz estática generalmente no tienen sentido, consulte #13462

En la discusión de #13462 no vi por qué los métodos de interfaz static no tienen sentido. Solo vi que su funcionalidad se puede implementar por otros medios (lo que demuestra que no tienen sentido).

Desde un punto de vista práctico, la interfaz es un tipo de especificación: un cierto conjunto de métodos que son obligatorios para su implementación en una clase que implementa esta interfaz. La interfaz no solo define la funcionalidad que proporciona un objeto, también es una especie de contrato.

Así que no veo ninguna razón lógica por la que class pueda tener el método static y interface no.

Si seguimos el punto de vista de que todo lo que ya se puede implementar en el lenguaje no necesita mejoras de sintaxis y otras cosas (es decir, este argumento fue uno de los puntos principales en la discusión de #13462), entonces guiado por esto punto de vista, podemos decidir que el ciclo while es redundante porque se puede implementar usando for y if juntos. Pero no vamos a acabar con while .

Una pregunta importante que tuvimos al considerar esto: ¿Quién puede llamar a un método abstract static ? Presumiblemente, no puede invocar AbstractParentClass.getSomeClassDependentValue directamente. Pero, ¿puede invocar el método en una expresión de tipo AbstractParentClass ? Si es así, ¿por qué debería permitirse eso? Si no, ¿cuál es el uso de la función?

Buena pregunta. Ya que estaba considerando este problema, ¿podría compartir sus ideas al respecto?

Solo me viene a la mente que, en el nivel del compilador, no se rastreará el caso de llamada directa de AbstractParentClass.getSomeClassDependentValue (porque no se puede rastrear), y se producirá el error de tiempo de ejecución de JS. Pero no estoy seguro de si esto es consistente con la ideología de TypeScript.

Solo vi que su funcionalidad se puede implementar por otros medios (lo que demuestra que no tienen sentido).

El hecho de que algo sea implementable no significa que tenga sentido. 😉

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

Quiero forzar la implementación del método de deserialización estática en las subclases de Serializable.
¿Hay alguna solución para implementar tal comportamiento?

¿Cuál es la última versión de esto? Solo traté de escribir una propiedad estática abstracta de una clase abstracta y me sorprendió genuinamente cuando no estaba permitido.

Lo que dijo @ patryk-zielinski93. Viniendo de algunos años de proyectos en PHP que convertimos a TS, SÍ queremos métodos estáticos en las interfaces.

Un caso de uso muy común con el que ayudará son los componentes de React, que son clases con propiedades estáticas como displayName , propTypes y defaultProps .

Debido a esta limitación, los tipos de React actualmente incluyen dos tipos: una clase Component #$3$#$ y una interfaz ComponentClass que incluye la función constructora y las propiedades estáticas.

Para verificar completamente el tipo de un componente React con todas sus propiedades estáticas, uno tiene dos usos de ambos tipos.

Ejemplo sin ComponentClass : se ignoran las propiedades estáticas

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

Ejemplo con ComponentClass : las propiedades estáticas tienen verificación de 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 }
  };
}

Sospecho que muchas personas actualmente no están usando ComponentClass , sin saber que sus propiedades estáticas no están siendo revisadas.

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

¿Hay algún progreso en esto? ¿O alguna solución para constructores en clases abstractas?
Aquí hay un ejemplo:

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!

Estuvo de acuerdo en que esta es una característica extremadamente poderosa y útil. En mi opinión, esta característica es lo que hace que las clases sean 'ciudadanos de primera clase'. La herencia de clase/métodos estáticos puede y tiene sentido, particularmente para el patrón de fábrica de métodos estáticos, que ha sido mencionado aquí por otros carteles varias veces. Este patrón es especialmente útil para la deserialización, que es una operación que se realiza con frecuencia en TypeScript. Por ejemplo, tiene mucho sentido querer definir una interfaz que proporcione un contrato que establezca que todos los tipos de implementación son instanciables desde JSON.

No permitir métodos de fábrica estáticos abstractos requiere que el implementador cree clases de fábrica abstractas en su lugar, duplicando innecesariamente el número de definiciones de clase. Y, como han señalado otros carteles, esta es una característica poderosa y exitosa implementada en otros lenguajes, como PHP y Python.

Nuevo en TypeScript, pero también me sorprende que esto no esté permitido de forma predeterminada, y que tanta gente intente justificar no agregar la función con:

  1. TS no necesita la función, porque aún puede lograr lo que está tratando de hacer a través de otros medios (lo cual es solo un argumento válido si proporciona un ejemplo de una manera muy objetivamente mejor de hacer algo, de la cual he visto muy poco)
  2. Que podamos no significa que debamos hacerlo. Genial: pero la gente está publicando ejemplos específicos de cómo sería útil/beneficioso. No veo cómo podría doler permitirlo.

Otro caso de uso simple: (forma ideal, que no 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 }
}

En cambio, la instancia de la lista termina exponiendo el tipo de instancia directamente, que no es relevante para la instancia de la lista en sí y se debe acceder a través del constructor. Se siente sucio. Se sentiría mucho más sucio si estuviéramos hablando de un modelo de registro, en el que se especifica un conjunto de valores predeterminados a través del mismo tipo de mecanismo, etc.

Estaba realmente emocionado de usar clases abstractas reales en un idioma después de estar en Ruby/Javascript durante tanto tiempo, solo para terminar consternado por las restricciones de implementación: el ejemplo anterior fue solo mi primer ejemplo de encontrarlo, aunque puedo pensar en muchos otros casos de uso donde sería útil. Principalmente, solo como un medio para crear DSL/configuraciones simples como parte de la interfaz estática, asegurando que una clase especifique un objeto de valores predeterminados o lo que sea. - Y puede que estés pensando, bueno, para eso están las interfaces. Pero para algo simple como esto, realmente no tiene sentido, y solo termina haciendo que las cosas sean más complicadas de lo que deberían ser (la subclase necesitaría extender la clase abstracta e implementar alguna interfaz, hace que nombrar las cosas sea más complicado, etc.).

Tuve este requisito similar para mi proyecto dos veces. Ambos estaban relacionados para garantizar que todas las subclases proporcionaran implementaciones concretas de un conjunto de métodos estáticos. Mi escenario se describe a continuación:

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 esto pueda explicar mis requisitos. Gracias.

Para cualquiera que busque una solución alternativa , puede 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}}) => {

dado que llamamos a través de un parámetro de tipo, la clase base real con su método de fábrica hipotético abstract static nunca estará involucrada. Los tipos de las subclases están relacionados estructuralmente cuando se instancia el parámetro de tipo.

Lo que no veo en esta discusión son casos en los que la implementación se invoca a través del tipo de la clase base en lugar de algún tipo sintetizado.

También es importante tener en cuenta que, a diferencia de lenguajes como C#, donde los miembros abstract son en realidad _anulados_ por sus implementaciones en clases derivadas, implementaciones de miembros de JavaScript, instancias o de otro tipo, nunca anulan los números heredados, sino que los _sombrean_.

Mis 5 centavos aquí, desde el punto de vista de la perspectiva del usuario son:

Para el caso de las interfaces, se debe permitir el modificador _static_

Las interfaces definen contratos, que se cumplirán mediante la implementación de clases. Esto significa que las interfaces son abstracciones en un nivel más alto que las clases.

En este momento, lo que puedo ver es que las clases pueden ser más expresivas que las interfaces, de manera que podemos tener métodos estáticos en las clases pero no podemos imponer eso, desde la definición del contrato en sí.

En mi opinión, eso se siente mal y obstaculiza.

¿Hay algún antecedente técnico que haga que esta característica del lenguaje sea difícil o imposible de implementar?

salud

Las interfaces definen contratos, que se cumplirán mediante la implementación de clases. Esto significa que las interfaces son abstracciones en un nivel más alto que las clases.

Las clases tienen dos interfaces, dos contratos de implementación, y eso es imposible.
Hay razones por las que lenguajes como C# tampoco tienen miembros estáticos en las interfaces. Lógicamente, las interfaces son la superficie pública de un objeto. Una interfaz describe la superficie pública de un objeto. Eso significa que no contiene nada que no esté allí. En instancias de clases, los métodos estáticos no están presentes en la instancia. Solo existen en la función de clase/constructor, por lo tanto, solo deben describirse en esa interfaz.

En instancias de clases, los métodos estáticos no están presentes en la instancia.

¿Puedes profundizar sobre eso? Esto no es C#

Solo existen en la función de clase/constructor, por lo tanto, solo deben describirse en esa interfaz.

Que lo conseguimos y es lo que queremos cambiar.

Las interfaces definen contratos, que se cumplirán mediante la implementación de clases. Esto significa que las interfaces son abstracciones en un nivel más alto que las clases.

Estoy completamente de acuerdo en eso. Ver el ejemplo de la interfaz Json

Hola @kitsonk , ¿podrías dar más detalles sobre:

Las clases tienen dos interfaces, dos contratos de implementación, y eso es imposible.

No entendí esa parte.

Lógicamente, las interfaces son la superficie pública de un objeto. Una interfaz describe la superficie pública de un objeto. Eso significa que no contiene nada que no esté allí.

Estoy de acuerdo. No veo ninguna contradicción en lo que digo. Incluso dije más. Dije que una interfaz es un contrato para que se cumpla una clase .

En instancias de clases, los métodos estáticos no están presentes en la instancia. Solo existen en la función de clase/constructor, por lo tanto, solo deben describirse en esa interfaz.

No estoy seguro de haber entendido bien, está claro lo que dice, pero no por qué lo dices. Puedo explicar por qué creo que sigue siendo válida mi declaración. Estoy pasando interfaces como parámetros todo el tiempo a mis métodos, esto significa que tengo acceso a métodos de interfaz, tenga en cuenta que no estoy usando interfaces aquí como una forma de definir la estructura de datos sino para definir objetos concretos que se crean/hidratan en otro lugar . Entonces cuando tengo:

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

En ese cuerpo de método, de hecho puedo usar métodos account . Lo que me gustaría poder hacer es poder usar métodos estáticos de SalesRepresentativeInterface que ya se aplicaron para implementarse en cualquier clase que esté recibiendo en account . Tal vez tengo una idea muy simplista o completamente equivocada sobre cómo usar la función.

Creo que permitir el modificador static me permitirá hacer algo como: SalesRepresentativeInterface.staticMethodCall()

Me equivoco ?

salud

@davidmpaz : su sintaxis no es del todo correcta, pero está cerca. Aquí hay un patrón de uso de ejemplo:

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);

Desafortunadamente, para que esto funcione, necesitamos dos características de TypeScript: (1) métodos estáticos en interfaces y métodos estáticos abstractos; (2) la capacidad de usar typeof como sugerencia de tipo con clases genéricas.

@davidmpaz

No entendí esa parte.

Las clases tienen dos interfaces. La función constructora y el prototipo de instancia. La palabra clave class esencialmente es azúcar para eso.

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

¿Puedes profundizar sobre eso? Esto no es C#

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

const foo = new Foo();

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

No hay .baz() en instancias de Foo . .baz() solo existe en el constructor.

Desde una perspectiva de tipo, puede hacer referencia/extraer estas dos interfaces. Foo se refiere a la interfaz de la instancia pública y typeof Foo se refiere a la interfaz del constructor público (que incluiría los métodos estáticos).

Para continuar con la explicación de @kitsonk , aquí está el ejemplo anterior reescrito para lograr lo que desea:

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); 

Tenga en cuenta aquí que:

  • La cláusula implements no es realmente necesaria, cada vez que usa la clase, se realiza una verificación estructural y la clase se validará para que coincida con la interfaz necesaria.
  • no necesita typeof para obtener el tipo de constructor, solo asegúrese de que su T sea lo que pretende capturar

@mhegazy : tuvo que eliminar mi llamada toJSON en la interfaz JSONSerializable. Aunque mi función de ejemplo simple makeInstance solo llama a fromJSON , es muy común querer crear una instancia de un objeto y luego usarlo. Has eliminado mi capacidad de hacer llamadas sobre lo que devuelve makeInstance , porque en realidad no sé qué T hay dentro makeInstance (particularmente relevante si makeInstance es un método de clase que se usa internamente para crear una instancia y luego usarla). Por supuesto que podría hacer esto:

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);

Y ahora sé que mi T tendrá todos los métodos disponibles en JSONSerializer . Pero esto es demasiado detallado y difícil de razonar (espera, estás pasando ImplementsJSONSerializer , pero esto no es un JSONSerializable , ¿verdad? Espera, ¿estás escribiendo pato?) . La versión más fácil de razonar es:

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());

Pero como señalé en un comentario anterior, ahora debemos crear una clase de fábrica para cada clase que queremos instanciar, lo que es aún más detallado que el ejemplo de tipeo de pato. Sin duda, ese es un patrón viable hecho en Java todo el tiempo (y supongo que C # también). Pero es innecesariamente detallado y duplicado. Todo ese repetitivo desaparece si se permiten métodos estáticos en interfaces y métodos estáticos abstractos en clases abstractas.

no hay necesidad de la clase adicional. las clases pueden tener static miembros... simplemente no necesita la cláusula implements . en un sistema de tipo nominal, realmente lo necesita para afirmar la relación "es un" en su clase. en un sistema de tipo estructural, realmente no necesita eso. la relación "es un" se verifica en cada uso de todos modos, por lo que puede eliminar con seguridad la cláusula implements y no perder la seguridad.

en otras palabras, puede tener una clase que tenga un fromJSON estático y cuyas instancias tengan un 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 Ya lo señalé como una opción viable. Vea mi primer ejemplo de 'escribir pato'. Argumenté que es innecesariamente detallado y difícil de razonar. Luego afirmé que la versión sobre la que es más fácil razonar (clases de fábrica) es aún más detallada.

Nadie está en desacuerdo con que es posible evitar la falta de métodos estáticos en las interfaces. De hecho, diría que hay una solución más elegante al usar estilos funcionales en lugar de clases de fábrica, aunque esa es mi preferencia personal.

Pero, en mi opinión, la forma más limpia y fácil de razonar para implementar este patrón excepcionalmente común es usar métodos estáticos abstractos. Aquellos de nosotros que hemos llegado a amar esta función en otros lenguajes (Python, PHP) extrañamos tenerla en TypeScript. Gracias por tu tiempo.

Luego afirmé que la versión sobre la que es más fácil razonar (clases de fábrica)

No estoy seguro de estar de acuerdo en que esto es más fácil de razonar.

Pero, en mi opinión, la forma más limpia y fácil de razonar para implementar este patrón excepcionalmente común es usar métodos estáticos abstractos. Aquellos de nosotros que hemos llegado a amar esta función en otros lenguajes (Python, PHP) extrañamos tenerla en TypeScript.

Y este problema está rastreando para que se agregue esto. Solo quería asegurarme de que los futuros visitantes de este hilo entiendan que esto es factible hoy sin una cláusula implements .

@kitsonk cierto, me perdí eso.

Chicos, tengo como LocationModule, que debería tener un método para obtener opciones para almacenar en la base de datos, a partir de la cual debería recrearse.
Cada LocationModule tiene sus propias opciones con tipo de recreación.

Así que ahora tengo que crear una fábrica con genéricos para recreación e incluso entonces no tengo verificaciones de tiempo de compilación. Ya sabes, superando el propósito aquí.

Al principio estaba como "bueno, no hay estática para las interfaces, así que sigamos con la clase abstracta, PERO incluso esto sería sucio, ya que ahora tengo que cambiar en todas partes en mi tipo de código de la interfaz a la clase abstracta para que el verificador reconozca los métodos que estoy buscando". .

Aquí:

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

Y luego me tropiezo con bueno, lo estático no puede ser abstracto. Y no puede estar en la interfaz.

Chicos, en serio, ¿no estamos protegiendo no implementar esto solo por no implementar esto?

Me encanta TypeScript apasionadamente. Como loco quiero decir. Pero siempre hay pero.

En lugar de tener una implementación forzada del método estático, tendría que verificar dos veces como lo estaba haciendo con todo en JavaScript simple y antiguo.

La arquitectura ya está llena de patrones, decisiones geniales, etc., pero algunas de ellas son solo para superar problemas con el uso de cosas tan simples como el método estático de interfaz.

Mi fábrica tendrá que verificar la existencia del método estático en tiempo de ejecución. ¿Como es eso?

O mejor:

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
  dontForgetToHaveStaticMethodForRecreation();
}

Si está utilizando el objeto de clase de forma polimórfica, que es la única razón por la que implementa una interfaz, no importa si la(s) interfaz(es) que especifica(n) los requisitos para el lado estático de la clase son referenciadas sintácticamente por la propia clase. porque se verificará en el sitio de uso y emitirá un error de tiempo de diseño si no se implementa la interfaz requerida.

@malina-kirn

Vea mi primer ejemplo de 'escribir pato'.

Este problema no tiene relación con si se usa o no el tipo de pato. TypeScript es tipo pato.

@aluanhaddad Tu implicación no es correcta. Puedo entender su punto de que la función principal implements ISomeInterface es usar el objeto de clase polimórficamente. Sin embargo, PUEDE importar si la clase hace referencia a las interfaces que describen la forma estática del objeto de la clase.

Da a entender que la inferencia de tipo implícito y los errores del sitio de uso cubren todos los casos de uso.
Considere este ejemplo:

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, {});

En el ejemplo anterior, se requiere una declaración de tipo explícita; no describir la forma del objeto de clase (lado estático) da como resultado un error de tiempo de diseño en el sitio de uso aunque los valores reales se ajusten a la forma esperada.

Además, la falta de una forma de hacer referencia a las interfaces que describen el lado estático, nos deja con la única opción de declarar explícitamente el tipo en cada miembro individual; y luego repetir eso en cada clase.

Sobre la base de mi publicación anterior, creo que el modificador static en las interfaces es una muy buena solución para algunos casos de uso que deben resolverse. Pido disculpas por lo grande que ha crecido este comentario, pero hay muchas cosas que quiero ilustrar.

1) Hay momentos en los que necesitamos poder describir explícitamente la forma del lado estático de un objeto de clase, como en mi ejemplo anterior.
2) Hay momentos en los que la verificación de forma en el sitio de definición es muy deseable, y momentos como en el ejemplo de @OliverJash donde el sitio de uso está en un código externo que no se verificará y necesita verificar la forma en el sitio de definición .

Con respecto al número 2, muchas publicaciones que he leído sugieren verificar la forma ya que el sitio de uso es más que suficiente. Pero en los casos en que el sitio de uso está en un módulo de galaxia muy, muy lejos... o cuando el sitio de uso está en una ubicación externa que no se verificará, obviamente, este no es el caso.

Otras publicaciones sugieren #workarounds para verificar la forma en el sitio de definición. Si bien estas temidas soluciones le permitirán verificar la forma en el sitio de definición (en algunos casos), existen problemas:

  • No resuelven el número 1 anterior, aún necesitaría declarar explícitamente el tipo en cada miembro en cada clase.
  • En muchos casos carecen de claridad, elegancia y legibilidad. No siempre son fáciles de usar, no son intuitivos y puede llevar bastante tiempo darse cuenta de que son posibles.
  • No funcionan en todos los casos y encontrar #workaroundsToMakeTheWorkaroundsWork no es divertido... o productivo... pero sobre todo no es divertido.

Aquí hay un ejemplo de la solución que he visto últimamente para forzar la verificación de formas en el sitio de definición...

// (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 {
...
}

Ahora intente usar esa solución alternativa con una clase abstracta incluida

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

Supongo que también solucionaremos eso... 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;

¡Pero espera hay mas! Veamos qué pasa si estamos 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> {
...
}

En este punto, si tiene un sitio de uso dentro del código que se verificará mediante mecanografiado, probablemente debería simplemente morder la bala y confiar en eso, incluso si está muy lejos.

Si su sitio de uso es externo, convenza a la comunidad/mantenedores para que acepten el modificador static para los miembros de la interfaz. Y mientras tanto, necesitará otra solución. No está en el sitio de definición, pero creo que puede usar una versión modificada de una solución alternativa descrita en #8328 para lograrlo. Vea la solución en el comentario de @mhegazy el 16 de mayo de 2016 cortesía de @RyanCavanaugh

Perdóname si me estoy perdiendo puntos clave. Mi comprensión de la vacilación para admitir static para los miembros de la interfaz es principalmente una aversión a usar la misma palabra clave de interfaz + implementos para describir tanto la forma del constructor como la forma de la instancia.

Permítanme presentar la siguiente analogía diciendo que me encanta lo que ofrece TypeScript y aprecio mucho a quienes lo han desarrollado y a la comunidad que pone mucho pensamiento y esfuerzo en lidiar con la gran cantidad de solicitudes de funciones; haciendo todo lo posible para implementar las características que encajan bien.

Pero, la vacilación en este caso me parece un deseo de preservar la 'pureza conceptual' a costa de la usabilidad. Nuevamente, lo siento si esta analogía está fuera de lugar:

Esto recordó el cambio de Java a C# . Cuando vi por primera vez el código C# haciendo esto
C# var something = "something"; if(something == "something") { ... }
las campanas de alarma comenzaron a sonar en mi cabeza, el mundo se estaba acabando, necesitaban estar usando "something".Equals(something) , etc.

¡ == es igual a referencia! Tiene un .Equals(...) separado para la comparación de cadenas...
y el literal de cadena debe ser lo primero para que no obtenga una referencia nula llamando a .Equals (...) en una variable nula ...
y... y... hiperventilando

Y luego, después de un par de semanas de usar C# , me di cuenta de lo fácil que era usarlo debido a ese comportamiento. Aunque significó renunciar a la distinción clara entre los dos, hace una mejora tan dramática en la usabilidad que vale la pena.

Así es como me siento acerca del modificador static para los miembros de la interfaz. Nos permitirá continuar describiendo la forma de la instancia como ya lo hemos hecho, nos permitirá describir la forma de la función constructora que solo podemos hacer caso por caso con soluciones alternativas deficientes, y nos permitirá hacer ambas cosas de manera relativamente limpia y sencilla. manera. Una gran mejora en la usabilidad, en mi opinión.

Evaluaré abstract static en el siguiente comentario....

Las interfaces siempre requieren una nueva especificación de los tipos de miembros en las clases de implementación. (la inferencia de las firmas de los miembros en la implementación _explícita_ de clases es una característica separada, que aún no se admite).

El motivo por el que recibe errores no está relacionado con esta propuesta. Para evitarlo, debe usar el modificador readonly en la implementación de los miembros de la clase, estáticos o de otro tipo, que deben ser de tipos literales; de lo contrario, se deducirá string .

Una vez más, la palabra clave implements es solo una especificación de la intención, no influye en la compatibilidad estructural de los tipos ni introduce tipos nominales. Eso no quiere decir que no pueda ser útil, pero no cambia los tipos.

Así que abstract static ... No vale la pena. Demasiados problemas.

Necesitaría una sintaxis nueva y compleja para declarar lo que ya se verificará implícitamente en el sitio de uso (si el compilador de mecanografiado verificará el sitio de uso).

Si el sitio de uso es externo, entonces lo que realmente necesita son static miembros de una interfaz... Lo siento, terminé de conectar... ¡y buenas noches!

@aluanhaddad Cierto, pero incluso si la inferencia de firmas de miembros en la implementación explícita de clases fuera una función admitida, carecemos de formas claras de declarar las firmas de miembros para el lado estático de la clase.

El punto que estaba tratando de expresar era que nos faltan formas de explicitly declarar la estructura esperada del lado estático de la clase y hay casos en los que eso sí importa (o importará para admitir características)

Principalmente, quería refutar esta parte de su comentario "no importa si la (s) interfaz (es) que especifica (n) los requisitos para el lado estático de la clase están referenciadas sintácticamente".

Estaba tratando de usar la inferencia de tipos como un ejemplo de por qué sería importante con el soporte futuro.
Al hacer referencia sintácticamente a la interfaz aquí let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' } , no tenemos que declarar explícitamente el tipo como 'Sí' | 'No' 100 veces separadas.

Para lograr la misma inferencia de tipo para el lado estático de una clase (en el futuro), necesitaremos alguna forma de hacer referencia sintácticamente a la(s) interfaz(es).

Después de leer su último comentario, creo que ese no fue un ejemplo tan claro como esperaba. Quizás un mejor ejemplo es cuando el sitio de uso para el lado estático de la clase está en un código externo que no será verificado por el compilador de texto mecanografiado. En tal caso, actualmente tenemos que confiar en soluciones alternativas que esencialmente crean un sitio de uso artificial, brindan una usabilidad/legibilidad deficiente y no funcionan en muchos casos.

A expensas de cierta verbosidad, puede lograr un nivel decente de seguridad de tipos:

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

De acuerdo, esto debería permitirse. El siguiente caso de uso simple se beneficiaría de métodos estáticos abstractos:

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 No creo que nadie haya mencionado esto exactamente todavía (aunque podría haberlo pasado por alto en mi lectura rápida), pero en respuesta a:

Una pregunta importante que tuvimos al considerar esto: ¿A quién se le permite llamar a un método estático abstracto? Presumiblemente, no puede invocar AbstractParentClass.getSomeClassDependentValue directamente. Pero, ¿puede invocar el método en una expresión de tipo AbstractParentClass? Si es así, ¿por qué debería permitirse eso? Si no, ¿cuál es el uso de la función?

Esto podría solucionarse implementando parte de #3841. Al leer ese problema, la objeción principal planteada parece ser que los tipos de funciones constructoras de clases derivadas a menudo no son compatibles con las funciones constructoras de sus clases base. Sin embargo, la misma preocupación no parece aplicarse a ningún otro método o campo estático porque TypeScript ya está comprobando que las estáticas anuladas sean compatibles con sus tipos en la clase base.

Entonces, lo que propongo es darle a T.constructor el tipo Function & {{ statics on T }} . Eso permitiría que las clases abstractas que declaran un campo abstract static foo accedan de forma segura a través this.constructor.foo sin causar problemas con las incompatibilidades del constructor.

E incluso si no se implementa la adición automática de estática a T.constructor , aún podríamos usar las propiedades abstract static en la clase base declarando manualmente "constructor": typeof AbstractParentClass .

Creo que muchas personas esperan que el ejemplo @ patryk-zielinski93 funcione. En su lugar, deberíamos usar soluciones contrarias a la intuición, detalladas y crípticas. Dado que ya tenemos clases de 'sintaxis azucarada' y miembros estáticos, ¿por qué no podemos tener tal azúcar en el sistema de tipos?

Aquí está mi dolor:

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

¿Tal vez podríamos introducir en su lugar/en paralelo una cláusula static implements ? p.ej

interface Foo {
    bar(): number;
}

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

Esto tiene la ventaja de ser compatible con versiones anteriores al declarar interfaces separadas para miembros estáticos y de instancia, lo que parece ser la solución alternativa actual, si estoy leyendo este hilo correctamente. También es una característica sutilmente diferente que podría ser útil por derecho propio, pero eso no viene al caso.

( statically implements sería mejor, pero eso introduce una nueva palabra clave. Es discutible si vale la pena. Sin embargo, podría ser contextual).

No importa cuánto traté de entender los argumentos de aquellos que son escépticos sobre la propuesta de OP, fracasé.

Solo una persona (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) te respondió @RyanCavanaugh . Reescribí el código de OP para hacerlo un poco más limpio y también para ilustrar su pregunta y mis respuestas:

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

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

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

¿Quién puede llamar a un método estático abstracto? Presumiblemente, no puede invocar Base.foo directamente

Si te refieres a ref # 1, entonces sí, nunca debería ser invocable, porque bueno, es abstracto y ni siquiera tiene un cuerpo.
Solo se le permite llamar a ref#2.

Pero, ¿puede invocar el método en una expresión de tipo Base?
Si es así, ¿por qué debería permitirse eso?

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 es un error. No, no puede invocar "foo" allí, porque se supone que baz es una __instancia__ de Child1 o Child2, y las instancias no tienen métodos estáticos

ref#4 es (debería ser) correcta. Puede (debería poder) invocar el método estático foo allí, porque se supone que baz es el constructor de Child1 o Child2, que amplían Base y, por lo tanto, deben haber implementado foo().

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

Puedes imaginarte esta situación:

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

¿Cuál es el uso de la función?

Ver ref#4 El uso es cuando no sabemos cuál de las clases secundarias recibimos como argumento (puede ser Child1 o Child2), pero queremos llamar a su método estático y asegurarnos de que ese método existe.
Estoy reescribiendo el marco node.js ahora mismo, llamado AdonisJS. Fue escrito en JS puro, así que lo estoy transformando a TS. No puedo cambiar cómo funciona el código, solo estoy agregando tipos. La falta de estas características me pone muy triste.

pd En este comentario, por razones de simplicidad, escribí solo sobre clases abstractas y no mencioné las interfaces. Pero todo lo que escribí es aplicable a las interfaces. Simplemente reemplaza la clase abstracta con la interfaz, y todo estará correcto. Existe la posibilidad, cuando el equipo de TS por alguna razón (no sé por qué) no quiera implementar abstract static en abstract class , sin embargo, estaría bien implementar solo static palabra en interfaces, eso nos haría bastante felices, supongo.

pps Edité los nombres de clase y método en sus preguntas para cumplir con mi código.

Un par de pensamientos, basados ​​en los principales argumentos de otros escépticos:

"No, esto no debe implementarse, pero ya puedes hacerlo de esta manera *escribe 15 veces más líneas de código*".

Para eso está hecho el azúcar sintáctico. Agreguemos un poco (azúcar) a TS. Pero no, será mejor torturar los cerebros de los desarrolladores que intentan romper los decoradores y una docena de genéricos en lugar de agregar una simple palabra static a las interfaces. ¿Por qué no considerar ese static como un azúcar más para hacernos la vida más fácil? De la misma manera que ES6 agrega class (que se vuelve omnipresente hoy en día). Pero no, seamos nerds y hagamos las cosas a la antigua y "bien".

"Hay dos interfaces para las clases js: para constructor e instancia".

Ok, ¿por qué no darnos una forma de hacer una interfaz para el constructor entonces? Sin embargo, simplemente agregar esa palabra static es mucho más fácil e intuitivo.

Y con respecto a esto:

Aplazando esto hasta que escuchemos más comentarios al respecto.

Ha pasado más de un año y se han proporcionado muchos comentarios, ¿durante cuánto tiempo se va a posponer este problema? Alguien puede responder, por favor.

Esta publicación puede parecer un poco dura... Pero no, es una solicitud del tipo que ama TS y, al mismo tiempo, realmente no puede encontrar una razón sólida por la que deberíamos optar por trucos feos, al convertir nuestra antigua base de código JS a TS. Aparte de esto, muchísimas gracias al equipo de TS. TS es simplemente maravilloso, cambió mi vida y disfruto programando y mi trabajo más que nunca... Pero este problema está envenenando mi experiencia...

¿Posible solución?

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

Propuesta: Miembros estáticos en interfaces y tipos, y miembros abstractos en clases abstractas, v2

casos de uso

Tipos compatibles con Fantasyland

````mecanografiado
interfaz Aplicativo se extiende Aplicar {estático de (a:A): Aplicativo ; }

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

tipo Serializable con método estático deserialize

````mecanografiado
interfaz Serializable {
deserialización estática(s: cadena): Serializable;

serialize(): string;

}
````

Contratos en general

````mecanografiado
Contrato de interfaz {
creación estática (x: número): Contrato;
estático nuevo (x: número): Contrato;
}

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

const c1 = factory1(ContractImpl); // Contract
const c2 = factory2(ContractImpl); // Contract
const c3 = factory3(ContractImpl); // ContractImpl
````

Sintaxis

Métodos y campos estáticos

````mecanografiado
interfaz Serializable {
deserialización estática(s: cadena): Serializable;
}

tipo Serializable = {
deserialización estática(s: cadena): Serializable;
};

clase abstracta Serializable {
deserialización abstracta estática (s: cadena): Serializable;
}
````

Firmas de constructores estáticos

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

Seguimiento: firmas de llamadas estáticas

Discutir:

¿Cómo se pueden expresar las firmas de llamadas estáticas?
Si los expresaremos simplemente agregando el modificador static antes de la firma de llamada,
¿Cómo los distinguiremos del método de instancia con el nombre 'static' ?
¿Podemos usar la misma solución que para los métodos con nombre 'new' ?
En tal caso, definitivamente será un cambio radical.

El operador de tipo static

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

El operador de tipo new

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

Ranuras internas

La ranura interna [[Static]]

La interfaz "estática" de un tipo se almacenará en la ranura interna [[Static]] :

````mecanografiado
// El tipo
interfaz Serializable {
deserialización estática(s: cadena): Serializable;
serializar: cadena;
}

// se representará internamente como
// interfaz Serializable {
// [[Estático]]: {
// [[Instancia]]: Serializable; // Vea abajo.
// deserializar(s: cadena): Serializable;
// };
// serializar(): cadena;
// }
````

Por defecto, tenga el tipo never .

La ranura interna [[Instance]]

Se almacenará la interfaz "Instancia" del tipo
en la ranura interna [[Instance]] del tipo de ranura interna [[Static]] .

Por defecto, tenga el tipo never .

Semántica

Sin el operador static

Cuando se utiliza un tipo como tipo de valor,
la ranura interna [[Static]] se descartará:

````mecanografiado
declare const serializable: Serializable;

tipo T = tipo de serializable;
// { serializar(): cadena; }
````

Pero el tipo en sí permanece manteniendo la ranura interna [[Static]] :

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

Asignabilidad

Ambos valores de tipos conscientes de estática son asignables a los estructuralmente idénticos
(excepto los de [[Static]] , por supuesto) y viceversa.

El operador static

| Asociatividad | Precedencia |
| :-----------: | :--------------------------------: |
| Derecha | IDK, pero es igual a la de new |

El operador de tipo static devuelve el tipo de ranura interna [[Static]] del tipo.
Es algo similar al operador de tipo typeof ,
pero su argumento debe ser un tipo en lugar de un valor.

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

El operador de tipo typeof también descarta la ranura interna [[Instance]] :

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

tipo T = tipo de SerializableImpl;
// { deserializar(s: cadena): Serializable; }
````

Asignabilidad

Ambos valores de tipos conscientes de instancias son asignables a los estructuralmente idénticos
(excepto la ranura interna [[Instance]] , por supuesto) y viceversa.

El operador new

| Asociatividad | Precedencia |
| :-----------: | :--------------------------------------------------: |
| Derecha | IDK, pero es igual a la de static |

Los operadores new devuelven el tipo de ranura interna [[Instance]] del tipo.
Invierte efectivamente el operador static :

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

extends / implements semántica

Una clase que implementa una interfaz con ranura interna [[Static]] no predeterminada
DEBE tener una ranura interna compatible [[Static]] .
Las comprobaciones de compatibilidad deben ser las mismas que (o similares a)
Comprobaciones regulares de compatibilidad (instancia).

````mecanografiado
clase SerializableImpl implementa Serializable {
deserialización estática(s: cadena): SerializableImpl {
// La lógica de deserialización va aquí.
}

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

serialize(): string {
    //
}

}
````

QUE HACER

  • [ ] Decida qué sintaxis debe usarse para las firmas de llamadas estáticas.
    Posiblemente sin romper los cambios.
  • [ ] ¿Existen casos especiales con tipos condicionales y operador infer ?
  • [ ] Cambios en la semántica de los miembros de la clase no abstractos. _Puede estar rompiendo._

Pasé algunas horas leyendo este problema y otros problemas para encontrar una solución a mi problema, que se resolvería con el modificador estático o las interfaces para las clases. No puedo encontrar una sola solución que resuelva mi problema.

El problema que tengo es que quiero hacer una modificación a una clase generada por código. Por ejemplo, lo que me gustaría hacer es:

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;

No puedo encontrar una sola solución que me permita hacer el equivalente a esto, con la versión actual de TS. ¿Me estoy perdiendo una manera de resolver este problema actualmente?

Esto se siente relevante para publicar aquí como un caso de uso para el cual aparentemente no hay una solución y ciertamente no hay una solución sencilla.

Si desea comparar la instancia y el lado estático de una clase con las interfaces, puede hacerlo así:

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();

Demasiados de nosotros estamos desperdiciando demasiadas horas de nuestro tiempo y de otros en esto. ¡¿Por qué no es una cosa?!¿i!
¿Por qué las respuestas son todas grandes soluciones para algo que debería ser legible, digerible y algo que es una sola línea que es sencillo? De ninguna manera quiero que alguien tenga que averiguar qué está tratando de hacer mi código con alguna solución alternativa. Perdón por abarrotar aún más este tema con algo que no es muy valioso, pero actualmente es un gran dolor y una pérdida de tiempo, así que lo encuentro valioso para mí, para los demás en este momento y para aquellos que vendrán más tarde buscando una respuesta.

Lo que pasa con cualquier lenguaje, tanto natural como artificial, es que debe evolucionar naturalmente para ser eficiente y atractivo para su uso. Eventualmente, la gente decidió usar reducciones en el lenguaje ("bien" => "bien", "va a" => "va a"), inventaron nuevas palabras ridículas, como "selfie" y "google", redefinieron la ortografía con l33tspeak y cosas, e incluso prohibió algunas palabras, y, a pesar de que quieras usarlas o no, todo el mundo todavía entiende lo que significan, y algunos de nosotros las usamos para lograr algunas tareas particulares. Y para ninguno de esos podría haber una buena razón, pero la eficiencia de ciertas personas en ciertas tareas, todo es cuestión de números de personas, que realmente los utilizan. El volumen de esta conversación muestra claramente que mucha gente podría hacer uso de este static abstract por las malditas consideraciones que tengan. Vine aquí por la misma razón, porque quería implementar Serializable así que probé todas las formas intuitivas (para mí) de hacerlo, y ninguna funcionó. Confía en mí, lo último que buscaría es la explicación de por qué no necesito esta función y debería optar por otra cosa. ¡Año y medio, Jesucristo! Apuesto a que ya hay un PR en alguna parte, con pruebas y esas cosas. Haga que esto suceda, y si hay una cierta forma de uso que no se recomienda, tenemos un tslint para eso.

Es posible acceder a miembros estáticos de clases secundarias desde la clase base a través this.constructor.staticMember , por lo que los miembros estáticos abstractos tienen sentido para mí.

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

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

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

La clase A debería poder especificar que requiere un miembro x estático, porque lo usa en el método f .

Hay noticias ?
Las características son realmente necesarias 😄
1) static funciones en interfaces
2) abstract static funciones en clases abstractas

aunque odio la idea de tener interfaces estáticas, pero para todos los propósitos prácticos, lo siguiente debería ser suficiente hoy :

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

que se puede utilizar como:

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

ACTUALIZAR:

más detalles sobre la idea y un ejemplo en 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, esto podría no ser culpa de Typescript, pero no pude hacer que estos decoradores de componentes angulares funcionaran y su compilador AoT.

@ aleksey-bykov eso es inteligente pero aún no funciona para la estática abstracta. Si tiene subclases de MyClass , no se aplican con la verificación de tipos. También es peor si tiene genéricos involucrados.

// no errors
class Thing extends MyClass {

}

Realmente espero que el equipo de TypeScript reconsidere su postura al respecto, porque la creación de bibliotecas de usuarios finales que requieren atributos estáticos no tiene una implementación razonable. Deberíamos poder tener un contrato que requiera que los implementadores de interfaz/extensores de clase abstracta tengan estática.

@bbugh cuestiono la existencia misma del problema que se discute aquí, ¿por qué necesitaría todos estos problemas con métodos heredados abstractos estáticos si se puede hacer lo mismo a través de instancias de clases 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

contra

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

@aleksey-bykov Hay muchas razones. Por ejemplo ( de @ patryk-zielinski93):

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

Quiero forzar la implementación del método de deserialización estática en las subclases de Serializable.
¿Hay alguna solución para implementar tal comportamiento?

EDITAR: Sé que también podría usar deserialize como constructor, pero las clases solo pueden tener 1 constructor, lo que hace que los métodos de fábrica sean necesarios. La gente quiere una forma de requerir métodos de fábrica en las interfaces, lo cual tiene mucho sentido.

simplemente lleve la lógica de deseo a una clase separada, porque no hay ningún beneficio en tener un método de deserialización estático adjunto a la misma clase que se está deserializando

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

¿Cuál es el problema?

@aleksey-bykov la clase separada no se ve tan bonita

@aleksey-bykov, el problema es que hay más de 1 clase que requiere serialización, por lo que su enfoque obliga a crear un diccionario de serializables, que es un antipatrón de clase superior, y cada modificación a cualquiera de ellos requeriría una edición en esta clase superior, lo que hace que el soporte de código sea una molestia. Si bien tener una interfaz serializable podría forzar una implementación para cualquier tipo de objeto dado, y también admitir la herencia polimórfica, que son las razones principales por las que la gente lo quiere. Por favor, no especulen si hay un beneficio o no, cualquiera debería poder tener opciones y elegir lo que es beneficioso para su propio proyecto.

@octaharon tener una clase dedicada únicamente a la deserialización es todo lo contrario de lo que dijiste, tiene una responsabilidad única porque la única vez que la cambias es cuando te preocupas por la deserialización

al contrario, agregar deserialización a la clase en sí, como usted propuso, le da una responsabilidad adicional y una razón adicional para cambiar

por último, no tengo problemas con los métodos estáticos, pero tengo mucha curiosidad por ver un ejemplo de un caso de uso práctico para métodos estáticos abstractos e interfaces estáticas

@octaharon tener una clase dedicada únicamente a la deserialización es todo lo contrario de lo que dijiste, tiene una responsabilidad única porque la única vez que la cambias es cuando te preocupas por la deserialización

excepto que tiene que cambiar el código en dos lugares en lugar de uno, ya que su (des) serialización depende de su estructura de tipo. Eso no cuenta para la "responsabilidad única"

al contrario, agregar deserialización a la clase en sí, como usted propuso, le da una responsabilidad adicional y una razón adicional para cambiar

No entiendo lo que estás diciendo. Una clase que es responsable de su propia (des) serialización es exactamente lo que quiero, y por favor, no me digas si está bien o mal.

la responsabilidad única no dice nada sobre cuántos archivos están involucrados, solo dice que tiene que haber una sola razón

lo que digo es que cuando agrega una nueva clase, lo dice para algo más que simplemente ser deserializado, por lo que ya tiene una razón para existir y una responsabilidad asignada; luego agrega otra responsabilidad de poder deserializarse, esto nos da 2 responsabilidades, esto viola SOLID, si sigue agregando más cosas como renderizar, imprimir, copiar, transferir, cifrar, etc., obtendrá una clase de dios

y disculpe si le digo banalidades, pero las preguntas (y respuestas) sobre los beneficios y los casos prácticos de uso es lo que impulsa la implementación de esta propuesta de función.

SOLID no fue enviado por Dios Todopoderoso en una tableta, e incluso si lo fuera, hay un grado de libertad en su interpretación. Puedes ser tan idealista como desees al respecto, pero hazme un favor: no difundas tus creencias en la comunidad. Todo el mundo tiene todos los derechos para usar cualquier herramienta de la manera que quiera y romper todas las reglas posibles que conoce (no se puede culpar a un cuchillo por un asesinato). Lo que define la calidad de una herramienta es el equilibrio entre la demanda de ciertas funciones y la oferta de las mismas. Y esta rama muestra el volumen de la demanda. Si no necesita esta función, no la use. Hago. Y un montón de gente aquí también lo necesita, y _nosotros_ tenemos un caso de uso práctico, mientras que usted dice que deberíamos ignorarlo, de hecho. Se trata solo de lo que es más importante para los mantenedores: los principios sagrados (de cualquier manera que lo entiendan) o la comunidad.

hombre, cualquier buena propuesta presenta casos de uso, esta no

image

así que solo expresé un poco de curiosidad e hice una pregunta, ya que la propuesta no lo dice, ¿por qué la gente podría necesitarla?

se redujo a lo típico: solo porque, no te atrevas

Personalmente, no me importa sólido o oop, simplemente lo crecí demasiado hace mucho tiempo, lo mencionaste lanzando el argumento "superclase antipatrón" y luego retrocediste a "un grado de libertad para su interpretación".

la única razón práctica mencionada en toda esta discusión es esta: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119

Un caso de uso muy común con el que ayudará son los componentes de React, que son clases con propiedades estáticas como displayName, propTypes y defaultProps.

y algunas publicaciones similares https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -345496014

pero está cubierto por (new (...) => MyClass) & MyStaticInterface

ese es el caso de uso exacto y una razón por la que estoy aquí. ¿Ves el número de votos? ¿Por qué crees que depende de ti personalmente decidir qué es practical y qué no? Practical es lo que se puede poner en practice , y (al momento de escribir esto) 83 personas encontrarían esta función muy práctica. Respete a los demás y lea el hilo completo antes de comenzar a sacar las frases de un contexto y exhibir varias palabras de moda. Lo que sea que hayas superado, definitivamente no es tu ego.

es de sentido común que las cosas prácticas son las que resuelven los problemas, las cosas no prácticas son las que estimulan tu sentido de la belleza, respeto a los demás, pero con todo ese respeto, la pregunta (sobre todo retórica ahora) sigue en pie: ¿qué problema pretende esta propuesta? resolver dado (new (...) => MyClass) & MyStaticInterface para https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119 y https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -289084844 solo porque

por favor no respondas

Por la misma razón que a veces uso declaraciones type para reducir anotaciones grandes, creo que una palabra clave como abstract static sería mucho más legible que una construcción relativamente más difícil de digerir como la que se ha mencionado como existente ejemplo.

Además, ¿todavía no hemos abordado las clases abstractas?

La solución para no usar clases abstractas no es la solución, en mi opinión. ¡Eso es una solución! ¿Una solución alternativa a qué?

Creo que esta solicitud de función existe porque muchas personas, incluido el solicitante, descubrieron que una función esperada como abstract static o static en las interfaces no estaba presente.

Según la solución ofrecida, ¿es necesario que exista la palabra clave static si existe una solución alternativa para evitar su uso? Creo que sería igualmente ridículo sugerirlo.

El problema aquí es que static tiene mucho más sentido.
Según el interés generado, ¿podemos tener una discusión menos desdeñosa?

¿Ha habido alguna actualización sobre esta propuesta? ¿Algún argumento que valga la pena considerar que demuestre por qué no deberíamos tener static abstract y similares?
¿Podemos tener más sugerencias que muestren por qué sería útil?

Tal vez necesitemos resumir las cosas y resumir lo que se ha discutido para que podamos encontrar una solución.

Hay dos propuestas según tengo entendido:

  1. las interfaces y los tipos pueden definir propiedades y métodos estáticos
interface ISerializable<T> { 
   static fromJson(json: string): T;
}
  1. las clases abstractas pueden definir métodos estáticos abstractos
abstract class MyClass<T> implements ISerializable<T> {
   abstract static fromJson(json: string): T;
}

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

En cuanto a la propuesta uno, ¡técnicamente hay una solución! Lo cual no es genial, pero eso es algo al menos. Pero ES una solución alternativa.

Puede dividir sus interfaces en dos y volver a escribir su clase como

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

interface InstanceInterface {
  toJson(): string;
}

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

En mi opinión, esto es mucho trabajo extra y un poco menos legible, y tiene la desventaja de volver a escribir sus clases de una manera divertida que es simplemente extraña y se desvía de la sintaxis que estamos usando.

Pero entonces, ¿qué pasa con la propuesta 2? No hay nada que se pueda hacer al respecto, ¿verdad? ¡Creo que eso también merece ser abordado!

¿Cuál es el uso práctico de uno de estos tipos? ¿Cómo se usaría uno de estos?

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

Ya es posible decir que un valor debe ser un objeto como { fromJSON(serializedValue: string): JsonSerializable; } , entonces, ¿se busca esto solo para hacer cumplir un patrón? No veo el beneficio de eso desde una perspectiva de verificación de tipos. Como nota al margen: en este caso, se estaría aplicando un patrón con el que es difícil trabajar; sería mejor mover el proceso de serialización a clases o funciones de serializador separadas por muchas razones que no abordaré aquí.

Además, ¿por qué se está haciendo algo así?

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

¿Qué hay de usar el método de plantilla o el patrón de estrategia en su lugar? Eso funcionaría y sería más flexible, ¿verdad?

Por el momento, estoy en contra de esta función porque me parece que agrega una complejidad innecesaria para describir diseños de clase con los que es difícil trabajar. ¿Tal vez hay algún beneficio que me estoy perdiendo?

hay un caso de uso válido para los métodos React estáticos, eso es todo

@aleksey-bykov ah, está bien. Para esos casos, podría ser mejor si agregar un tipo en la propiedad constructor provocaría la verificación de tipos en ese escenario poco común. Por ejemplo:

interface Component {
    constructor: ComponentConstructor;
}

interface ComponentConstructor {
    displayName?: string;
}

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

Eso me parece mucho más valioso. No agrega ninguna complejidad adicional al lenguaje y solo agrega más trabajo para el verificador de tipos al verificar si una clase implementa una interfaz correctamente.


Por cierto, estaba pensando que un caso de uso válido sería viajar de una instancia al constructor y finalmente a un método estático o propiedad con verificación de tipos, pero eso ya es posible escribiendo la propiedad constructor en un tipo y se resolverá para instancias de clase en #3841.

¿El patrón de serialización/deserialización descrito en el subproceso no es "válido"?

@dsherret no "ve el beneficio desde una perspectiva de verificación de tipos". El objetivo principal de hacer un análisis estático en primer lugar es detectar errores lo antes posible. Escribo las cosas para que, si es necesario cambiar la firma de una llamada, todos los que la llamen (o, lo que es más importante, todos los responsables de implementar métodos que usan la firma) actualicen a la nueva firma.

Supongamos que tengo una biblioteca que proporciona un conjunto de clases hermanas con un método estático foo(x: number, y: boolean, z: string) , y la expectativa es que los usuarios escriban una clase de fábrica que tome varias de estas clases y llame al método para construir instancias. (Tal vez sea deserialize o clone o unpack o loadFromServer , no importa). El usuario también crea subclases del mismo padre (posiblemente abstracto) clase de la biblioteca.

Ahora, necesito cambiar ese contrato para que el último parámetro sea un objeto de opciones. Pasar un valor de cadena para el tercer argumento es un error irrecuperable que debe marcarse en tiempo de compilación. Cualquier clase de fábrica debe actualizarse para pasar un objeto cuando se llama a foo , y las subclases que implementan foo deben cambiar su firma de llamada.

Quiero garantizar que los usuarios que actualicen a la nueva versión de la biblioteca captarán los cambios importantes en el momento de la compilación. La biblioteca tiene que exportar una de las soluciones de interfaz estática anteriores (como type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface; ) y esperar que el consumidor la haya aplicado en todos los lugares correctos. Si olvidaron decorar una de sus implementaciones o si no usaron un tipo exportado de biblioteca para describir la firma de llamada de foo en su clase de fábrica, el compilador no puede notar que nada cambió y obtienen errores de tiempo de ejecución . Compare eso con una implementación sensata de los métodos abstract static en la clase principal: no se requieren anotaciones especiales, no hay carga para el código de consumo, simplemente funciona de inmediato.

@thw0rted , ¿la mayoría de las bibliotecas no requerirán algún tipo de registro de estas clases y la verificación de tipo puede ocurrir en ese punto? Por ejemplo:

// 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);

¿O se utilizan instancias de estas clases con la biblioteca y la biblioteca pasa de la instancia al constructor para obtener los métodos estáticos? Si es así, es posible cambiar el diseño de la biblioteca para que no requiera métodos estáticos.

Como nota al margen, como implementador de una interfaz, me molestaría mucho si me vieran obligados a escribir métodos estáticos porque es muy difícil inyectar dependencias a un método estático debido a la falta de un constructor (las dependencias de ctor no son posibles). También dificulta el intercambio de la implementación de la deserialización con otra cosa según la situación. Por ejemplo, supongamos que estaba deserializando desde diferentes fuentes con diferentes mecanismos de serialización... ahora tengo un método de deserializado estático que, por diseño, solo se puede implementar de una manera para la implementación de una clase. Para evitar eso, necesitaría tener otra propiedad o método estático global en la clase para decirle al método de deserialización estática qué usar. Preferiría una interfaz Serializer separada que permitiría intercambiar fácilmente qué usar y no acoplaría la (des) serialización a la clase que se (des) serializa.

Veo lo que quiere decir: para usar un patrón de fábrica, eventualmente debe pasar la clase de implementación a la fábrica, y la verificación estática ocurre en ese momento. Todavía creo que puede haber momentos en los que desee proporcionar una clase compatible con la fábrica pero no usarla en este momento, en cuyo caso una restricción abstract static detectaría los problemas antes y aclararía el significado.

Tengo otro ejemplo que creo que no entra en conflicto con su preocupación de "nota al margen". Tengo varias clases de hermanos en un proyecto de interfaz, donde le doy al usuario la opción de qué "proveedor" usar para una característica determinada. Por supuesto, podría hacer un diccionario en algún lugar de name => Provider y usar las teclas para determinar qué mostrar en la lista de selección, pero la forma en que lo tengo implementado ahora es requiriendo un campo estático name en la implementación de cada proveedor.

En algún momento, cambié eso para requerir shortName y longName (para mostrar en diferentes contextos con diferentes cantidades de espacio de pantalla disponible). Habría sido mucho más sencillo cambiar abstract static name: string; a abstract static shortName: string; , etc., en lugar de cambiar el componente de lista de selección para tener un providerList: Type<Provider> & { shortName: string } & { longName: string } . Transmite la intención, en el lugar correcto (es decir, en el padre abstracto, no en el componente consumidor), y es fácil de leer y cambiar. Creo que podemos decir que hay una solución, pero sigo creyendo que es objetivamente peor que los cambios propuestos.

Recientemente me topé con este problema, cuando necesitaba usar un método estático en una interfaz. Pido disculpas, si esto ya se ha abordado antes, ya que no tengo tiempo para leer esta cantidad de comentarios.

Mi problema actual: tengo una biblioteca .js privada que quiero usar en mi proyecto de TypeScript. Así que seguí adelante y comencé a escribir un archivo .d.ts para esa biblioteca, pero como la biblioteca usa métodos estáticos, realmente no pude terminar eso. ¿Cuál es el enfoque sugerido en este caso?

Gracias por las respuestas.

@greeny aquí hay una solución funcional: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

específicamente esta parte de ella:

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

Otro caso de uso: código generado/calzas de clases parciales

  • Estoy usando la biblioteca @rsuter/nswag, que genera especificaciones de swagger.
  • Puede escribir un archivo de 'extensiones' que se fusione con el archivo generado.
  • El archivo de extensiones nunca se ejecuta, ¡pero debe compilarse solo!
  • A veces, dentro de ese archivo, necesito referirme a algo que aún no existe (porque se genera)
  • Por lo tanto, quiero declarar una cuña/interfaz para ello de la siguiente manera
  • Nota: Puede especificar que se ignoren ciertas instrucciones import en la combinación final, de modo que la inclusión de 'shim' se ignore en el código generado final.
interface SwaggerException
{
    static isSwaggerException(obj: any): obj is SwaggerException;
}

Pero no puedo hacer esto y tengo que escribir una clase, lo cual está bien pero aún se siente mal. Solo quiero, como muchos otros han dicho, decir 'este es el contrato'

Solo quería agregar esto aquí ya que no veo ninguna otra mención de la generación de código, pero muchos comentarios de "¿por qué necesitarías esto?" que simplemente se vuelven irritantes. Esperaría que un porcentaje decente de personas que 'necesitan' esta función 'estática' en una interfaz lo hagan solo para poder consultar elementos de bibliotecas externas.

También me gustaría ver archivos d.ts más limpios; debe haber cierta superposición con esta característica y esos archivos. Es difícil entender algo como JQueryStatic porque parece que es solo un truco. Además, la realidad es que los archivos d.ts a menudo están desactualizados y no se mantienen, y usted mismo debe declarar las correcciones de compatibilidad.

(perdón por mencionar jQuery)

Para el caso de serialización, hice algo así.

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

Pero sería aún mucho más fácil usar algo así:

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

He leído mucho este hilo y todavía no entiendo por qué es bueno y correcto decir que no a este patrón. Si comparte esa opinión, también dice que java y otros lenguajes populares lo hacen mal. ¿O estoy equivocado?

Este número lleva abierto dos años y tiene 79 comentarios. Está etiquetado como Awaiting More Feedback . ¿Podría decir qué más comentarios necesita para tomar una decisión?

es realmente molesto que no pueda describir el método estático ni en la interfaz ni en la clase abstracta (solo declaración). Es tan feo escribir soluciones porque esta función aún no se ha implementado =(

Por ejemplo, next.js usa una función estática getInitialProps para obtener las propiedades de la página antes de construir la página. En caso de que arroje, no se construye la página, sino la página de error.

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

Pero desafortunadamente, los ts de pistas que implementan este método pueden darle cualquier firma de tipo, incluso si causa errores en tiempo de ejecución, porque no se puede verificar el tipo.

Creo que este problema existe aquí desde hace tanto tiempo porque JavaScript en sí mismo no es bueno en cosas estáticas 🤔

La herencia estática nunca debería existir en primer lugar. 🤔🤔

¿Quién quiere llamar a un método estático o leer un campo estático? 🤔🤔🤔

  • clase infantil: no debe ser estático
  • clase padre: usar argumento constructor
  • otro: use la interfaz ISomeClassConstructor

¿Hay algún otro caso de uso? 🤔🤔🤔🤔

¿Alguna actualización sobre esto?

Si sirve de ayuda, lo que he hecho en los casos en que necesito escribir la interfaz estática para una clase es usar un decorador para hacer cumplir los miembros estáticos en la clase.

El decorador se define como:

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

Si tengo una interfaz de miembro de constructor estática definida como

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

e invocado en la clase que debe declarar estáticamente los miembros en T como:

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

El ejemplo más frecuente es bueno:

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

Pero como se dijo en #13462 aquí :

Las interfaces deben definir la funcionalidad que proporciona un objeto. Esta funcionalidad debe ser reemplazable e intercambiable (es por eso que los métodos de interfaz son virtuales). La estática es un concepto paralelo al comportamiento dinámico/métodos virtuales.

Estoy de acuerdo en el punto de que las interfaces, en TypeScript, describen solo una instancia de objeto en sí misma y cómo usarla. El problema es que una instancia de objeto no es una definición de clase , y un símbolo static solo puede existir en una definición de clase .

Así que puedo proponer lo siguiente, con todos sus defectos:


Una interfaz podría estar describiendo un objeto o una clase . Digamos que se anota una interfaz de clase con las palabras clave class_interface .

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

Las clases (y las interfaces de clase) podrían usar las palabras clave statically implements para declarar sus símbolos estáticos usando una interfaz de objeto (las interfaces de clase no podrían implementarse statically ).

Las clases (y las interfaces de clase) seguirían usando la palabra clave implements con una interfaz de objeto o una interfaz de clase .

Una interfaz de clase podría entonces ser la mezcla entre una interfaz de objeto implementada estáticamente y una interfaz implementada de instancia. Así, podríamos obtener lo siguiente:

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

class_interface ISerDes implements ISerializable statically implements IDeserializable {}

De esta forma, las interfaces podrían mantener su significado y class_interface sería un nuevo tipo de símbolo de abstracción dedicado a las definiciones de clases.

Un pequeño caso de uso no crítico más:
En Angular para la compilación AOT, no puede llamar a funciones en decoradores (como en el decorador de módulo @NgModule )
problema angular

Para el módulo de trabajador de servicio, necesita algo como esto:
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
Nuestro entorno usa subclases, extendiendo la clase abstracta con valores predeterminados y propiedades abstractas para implementar. Ejemplo de implementación similar
Entonces AOT no funciona porque construir una clase es una función y arroja un error como: Function calls are not supported in decorators but ..

Para que funcione y mantener la compatibilidad con el autocompletado/compilador, es posible definir las mismas propiedades en el nivel estático. Pero para 'implementar' propiedades, necesitamos una interfaz con miembros estáticos o miembros estáticos abstractos en clase abstracta. Ambos no son posibles todavía.

con interfaz podría funcionar de esta manera:

// 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;

con estática abstracta podría funcionar de esta manera:

// 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;

hay soluciones , pero todas son débiles:/

@DanielRosenwasser @RyanCavanaugh se disculpa por las menciones, pero parece que esta sugerencia de función, que cuenta con mucho apoyo de la comunidad y creo que sería bastante fácil de implementar, se ha enterrado profundamente en la categoría Problemas. ¿Alguno de ustedes tiene algún comentario acerca de esta característica, y sería bienvenido un PR?

¿No es este problema un duplicado del #1263? 😛

26398 (Miembros estáticos de verificación de tipo basados ​​en la propiedad de constructor de tipo de implementos) parece una mejor solución ... si se implementara algo como esto, entonces espero que sea ese. Eso no requiere ninguna sintaxis/análisis adicional y es solo un cambio de verificación de tipo para un solo escenario. Tampoco plantea tantas preguntas como esto.

Siento que los métodos estáticos en las interfaces no son tan intuitivos como tener métodos abstractos estáticos en clases abstractas.

Creo que es un poco incompleto agregar métodos estáticos a las interfaces porque una interfaz debe definir un objeto, no una clase. Por otro lado, una clase abstracta ciertamente debería poder tener métodos abstractos estáticos, ya que las clases abstractas se usan para definir subclases. En lo que respecta a la implementación de esto, simplemente necesitaría verificarse el tipo al extender la clase abstracta (por ejemplo class extends MyAbstractClass ), no cuando se usa como un tipo (por ejemplo let myInstance: MyAbstractClass ).

Ejemplo:

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

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

ahora mismo por necesidad uso esto

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();
  }
}

esto es inconveniente!

Vine con un problema en el que estoy agregando propiedades al "Objeto", aquí hay un ejemplo 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

Ahora se considera que cualquier instancia de objeto tiene la propiedad getInstanceId mientras que solo la debería tener Object . Con una propiedad estática, el problema se habría resuelto.

Desea aumentar ObjectConstructor, no Object. Está declarando un método de instancia, cuando en realidad desea adjuntar un método al propio constructor. Creo que esto ya es posible mediante la fusión de declaraciones:

````ts
declarar global {
interfaz ObjectConstructor {
hola(): cadena;
}
}

Objeto.hola();
````

@thw0rted ¡Excelente! gracias no conocía ObjectConstructor

El punto más importante es que está aumentando el tipo de constructor en lugar del tipo de instancia. Acabo de buscar la declaración Object en lib.es5.d.ts y descubrí que es del tipo ObjectConstructor .

Es difícil para mí creer que este problema todavía existe. Esta es una característica legítimamente útil, con varios casos de uso reales.

El objetivo de TypeScript es poder garantizar la seguridad de los tipos en nuestra base de código, entonces, ¿por qué esta función aún está "pendiente de comentarios" después de dos años de comentarios?

Puede que esté muy lejos de esto, pero ¿algo como las metaclases de Python podrían resolver este problema de una manera nativa y sancionada (es decir, no un truco o una solución alternativa) sin violar el paradigma de TypeScript (es decir, manteniendo tipos de TypeScript separados para la instancia y la clase)?

Algo como esto:

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, la parte más complicada de este enfoque parece ser la creación de una palabra clave significativa de TypeScript para metaclass ... staticimplements , classimplements , withstatic , implementsstatic ... no estoy seguro.

Esto es un poco como la propuesta de @GerkinDev pero sin el tipo de interfaz separada. Aquí, hay un solo concepto de interfaz, y pueden usarse para describir la forma de una instancia o de una clase. Las palabras clave en la definición de la clase de implementación le dirían al compilador de qué lado se debe verificar cada interfaz.

Reanudemos la discusión en #34516 y #33892 según la característica que elija

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

wmaurer picture wmaurer  ·  3Comentarios

MartynasZilinskas picture MartynasZilinskas  ·  3Comentarios

jbondc picture jbondc  ·  3Comentarios

uber5001 picture uber5001  ·  3Comentarios

dlaberge picture dlaberge  ·  3Comentarios