Typescript: Agregue interfaces de soporte para definir métodos estáticos

Creado en 13 ene. 2017  ·  43Comentarios  ·  Fuente: microsoft/TypeScript

Versión de TypeScript: 2.0.3

Código

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

Comportamiento esperado:
Sin errores
Comportamiento real:
No admitido

Question

Comentario más útil

@aluanhaddad Creo que tu código es una solución a algo que es obvio.

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

¿Cuál ves con más claridad?

Todos 43 comentarios

¿Qué quieres lograr con esto? ¿Puedes dar más detalles sobre el escenario?

Imagino que las clases de abstract son lo que estás buscando.

@mhegazy
Ejemplo:

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

@rozzzly En este ejemplo, abstract class no es válido.

¡Creo que esto sería genial! Java agregó esta funcionalidad en la última versión.

@Serginho
Creo que puede encontrar esto interesante:

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

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

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

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

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

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

const json = a.toJson();

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

@aluanhaddad Creo que tu código es una solución a algo que es obvio.

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

¿Cuál ves con más claridad?

@aluanhaddad Creo que tu código es una solución a algo que es obvio.

interfaz JsonSerializable {
public static fromJson (obj: any);
public toJson (): cadena;
}
¿Cuál ves con más claridad?

De hecho, pero el mío no es una solución alternativa, es una justificación para su caso de uso. Tener la deserialización estática y la serialización de instancias implementadas clase por clase y, por lo tanto, encapsuladas y con seguridad de tipos.
Tu declaración:

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

es irrelevante ya que básicamente vuelve a declarar la interfaz del objeto JSON. Eso absolutamente no requiere métodos de interfaz estáticos de ninguna manera.

Estás perdiendo el concepto de interfaz. class A implements JsonSerializable debería hacerme implementar ambos métodos. Pero en realidad me hace implementar:

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

No es una solución clara.

No hay ninguna razón técnica para no permitir la definición de métodos estáticos en interfaces.

@Serginho No estoy argumentando que la situación sea ideal. Solo estaba tratando de ilustrar que se puede expresar.

@aluanhaddad ¡ Vamos! Abre tu mente. ¿Crees que el mecanografiado debería permitir métodos estáticos en interfaces? esto se implementó en Java 8 en la última versión, así que creo que no estoy diciendo tonterías.

@Serginho No creo que sea una buena opción para TypeScript. Las interfaces deben definir la funcionalidad que proporciona un objeto. Esta funcionalidad debe ser reemplazable e intercambiable (por eso los métodos de interfaz son virtuales). La estática es un concepto paralelo al comportamiento dinámico / métodos virtuales. Entrelazar los dos no se siente bien desde el punto de vista del diseño para mí.

Como ya escribió @aluanhaddad , TypeScript en realidad ya tiene un mecanismo para expresar lo que desea. La gran diferencia es que en este caso se trata a la clase como un objeto, que sigue siendo lógicamente coherente. Desde mi perspectiva, el enfoque que propones no se adapta particularmente bien a TypeScript (y al desarrollo de JavaScript) ya que las clases son una especie de piratería / ciudadanos de segunda clase. Los patrones que prevalecen actualmente se basan en la programación de formas / tipos de patos, no en clases rígidas.

Supongo que también puedes abrir tu mente y probar diferentes estilos de programación. El mundo no comienza ni termina con OOP. Puede encontrar la programación con funciones, objetos simples y (y hasta cierto punto) prototipos placenteros e incluso mejores. Tal estilo está mucho más en línea con lo que JavaScript fue diseñado inicialmente. En una nota al margen, estoy de acuerdo en que JS está cambiando lentamente hacia patrones de programación orientada a objetos (al menos a nivel de comité), pero eso se debe al gran impulso de las personas que no se sienten cómodas con diferentes paradigmas de programación y técnicas de desarrollo. Para mí es una cosa profundamente triste y decepcionante.

Como dijo @gcnew , una interfaz describe la forma de un objeto individual, no su clase. Pero no hay razón para que no podamos usar una interfaz para describir la forma de la clase en sí, ya que las clases también son objetos.

import assert = require("assert");

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

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

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

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

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

No cumplir con la firma de toJson o fromJson da como resultado un error de compilación. (Desafortunadamente, el error está en const _ lugar de en el método).

Probablemente relacionado (ya que se trata de escribir métodos estáticos): # 5863.


@aluanhaddad : Su ejemplo tiene un error: JsonSerializable constructor member en realidad se referiría a una propiedad de instancia llamada constructor , que cuando se invoca con new devuelve un JsonSerializableStatic . Lo que eso significaría es que (new ((new X()).constructor)).fromJson({}) tendría que funcionar. La razón por la que se compila correctamente es que interface A extends JsonSerializable<typeof A> declara que la implementación es válida sin verificarla realmente. Por ejemplo, esto se compila sin errores:

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

@Serginho No es un usuario de Java, pero no parece que el lenguaje le permita definir una interfaz para el lado estático de una clase (lo que significa que las clases que implementan la interfaz tendrían que implementar un método estático para ajustarse). Java le permite definir un método estático con un cuerpo en una interfaz, cuyo equivalente en TypeScript sería:

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

¿Y si estuviera intentando hacer una fábrica de genéricos?

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

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

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

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

No estoy seguro de la sintaxis interface propongo aquí, pero sé que la sintaxis de "uso" es lo que estoy tratando de lograr. Este es un patrón que uso con frecuencia en Swift (usando protocols ) y creo que sería muy bueno en TypeScript. Aunque no soy un diseñador de lenguaje o un implementador de compiladores, no estoy seguro de si encaja en la dirección prevista de TypeScript o si es realista para su implementación.

La clase no tiene que implement la interfaz. simplemente define la interfaz, y las verificaciones de tipo estructural almacenarán en caché cualquier problema en el sitio de uso, por ejemplo:

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

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

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

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

@mhegazy esa es una solución relativamente buena. ¡Gracias por proporcionar eso! 🙏

Todavía hay dos cosas que me incomodan (hay que reconocer que ninguno de los dos lo es):

  1. Todavía no podemos declarar que Bar implementa explícitamente o se ajusta a Factorizable .

    • En la práctica, imagino que esto realmente no es un problema. Ya que sería cierto que si la interfaz Factorizable cambia de una manera incompatible, el uso x.bar(Bar) comenzaría a generar errores y luego corregiría los cambios de deriva.
  2. Para mí, sigue siendo una carga cognitiva declarar el tipo de y en el lado derecho de la tarea.

    • Más extraño aún, esta sintaxis permitiría este comportamiento: var y: Baz[] = x.bar(Bar) . Obviamente, es un error, pero la sintaxis permite al desarrollador restringir en exceso el problema definiendo el tipo de retorno en dos lugares.

Todavía no podemos declarar que Bar implementa explícitamente o cumple con Factorizable.

Hay dos tipos involucrados, 1. función constructora (por ejemplo, el lado estático de la clase) y 2. el lado de la instancia (lo que sale al llamar new ). Mezclar estos dos en un tipo no es correcto. En teoría, puede tener implements y static implements pero en la práctica, como notó, esto rara vez se usa y la cláusula de implements realmente no agrega mucho. La verificación se realiza en el sitio de uso de cualquier manera, independientemente de si tiene una cláusula implements o no.

Para mí, sigue siendo una carga cognitiva declarar el tipo de y en el lado derecho de la tarea.

Los dos significan cosas diferentes, var y = x.bar(Bar) declara una nueva variable y con el mismo tipo que x.bar(Bar) ; donde as var y: Bar[] = x.bar(Bar) declara una nueva variable y con el tipo Bar[] y verifica que el tipo de x.bar(Bar) se pueda asignar a Bar[] .

Habiendo dicho eso, esto es más bien una cuestión de estilo. Personalmente, mi recomendación es no usar anotaciones de tipo explícitamente a menos que sea necesario; deje que los tipos fluyan a través del sistema. Sin embargo, he visto bases de código donde la guía de estilo es la opuesta, donde todo tiene una anotación de tipo explícita.

@mhegazy gracias por la discusión / perspectiva.

@ andy-hanson Gracias por tomarse el tiempo para corregirme. Arreglé el error en mi ejemplo.

Esto también funciona y muestra un error en el momento de la compilación sin ninguna llamada de función adicional:

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

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

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

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

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

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

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

¿Cuál es el resultado de la discusión?

Me encontré con esto y también quiero usar static interface :

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

La mayoría de la gente olvida que ya existen interfaces _static_ en el sentido de que una función / clase de constructor ya tiene dos interfaces, la interfaz de constructor y la interfaz de instancia.

interface MyFoo {
  method(): void;
}

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

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

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

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

Si no está usando clases _abstract_, entonces ya tiene el poder.

Gracias a @kitsonk por responder.

Su declaración parece que debería funcionar, pero es demasiado detallada para el caso.

Y acabo de probar las clases _abstract_, pero parece que no es compatible con static con abstract .

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

@zixia ese es el número 14600

Sí, votémoslo.

¿A alguien le importaría responder con una solución a mi pregunta aquí: http://stackoverflow.com/questions/44047874/dynamically-modify-typescript-classes-through-a-generic-function

Creo que todo el problema se resolvería al poder especificar miembros estáticos en las interfaces, y si este problema se cierra porque no es necesario , me gustaría mucho ver cómo resolverlo.

@grantila He respondido a tu pregunta. Como se mencionó anteriormente en este número, a menos que tenga requisitos adicionales que no se mencionan allí, esto se puede resolver fácilmente tratando las clases como objetos.

@ Enet4 Actualicé la pregunta, estaba demasiado simplificada. Lamentablemente, el problema real no se puede solucionar con el truco Object.defineProperty() . Lo cual, por cierto, es un truco. Quiero asegurarme de no haber escrito mal make , básicamente una verificación estática adecuada.

Este es un problema real que tengo, código que comencé a migrar a TypeScript pero ahora lo he mantenido como JavaScript, ya que actualmente no puedo reescribir cantidades tan masivas de código como hubiera sido necesario de otra manera.

Quiero asegurarme de no tener una marca mal escrita, básicamente una verificación estática adecuada.

La comprobación estática solo puede llegar hasta aquí. Pero si su única preocupación ahora es que está configurando la propiedad correcta, entonces la conversión a un tipo de fabricante y la configuración de la propiedad desde allí parece abordar eso.

@ Enet4 es una solución funcional, gracias. Creo que este problema (13462) debería ser analizado nuevamente, ya que soluciones como la suya, usando conversión de tipos, en realidad no son seguras para tipos, y si esta es la única forma de resolver la situación de trabajar con el tipo de clase como valor, Estás perdiendo mucha flexibilidad de un lenguaje dinámico.

soluciones como la suya, que utilizan conversión de tipos, en realidad no son seguras para los tipos, y si esta es la única forma de resolver la situación de trabajar con el tipo de clase como valor, estamos perdiendo gran parte de la flexibilidad de un lenguaje dinámico.

@grantila En mi defensa, eso es discutible. : wink: Su caso de uso es distinto de los presentados en este número, ya que su tipo de clase puede (o no) proporcionar un método dependiendo de las condiciones de tiempo de ejecución. Y en mi opinión, eso es más inseguro que el tipo de conversión presentado en mi respuesta, que solo se realizó para permitir la inserción de un campo en un objeto. En ese sentido, el tipo de clase resultante C & Maker<T> debería seguir siendo compatible con todo lo demás que se base en un C .

También intenté imaginar dónde los métodos estáticos en las interfaces podrían ayudarlo aquí, pero es posible que me esté perdiendo algo. Incluso si tuviera algo como static make?(... args: any[]): self en su interfaz, tendría que comprobar su existencia en tiempo de ejecución antes de una llamada. Si desea continuar con esta discusión, consideremos hacerlo en otro lugar para reducir el ruido. : leve_sonriente_cara:

Entonces, ¿no podemos escribir métodos de fábrica estática de verificación en clases que implementan la misma interfaz?

Mi caso de uso es:

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

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

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

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

No quiero escribir una nueva clase de fábrica solo por esto.

@ tyteen4a03 Elimina IObject de ese ejemplo y se compilará. Consulte también https://github.com/Microsoft/TypeScript/issues/17545#issuecomment -319422545

@ andy-ms Sí, obviamente funciona, pero el objetivo de la verificación de tipos es ... verificar los tipos. Siempre puede degradar la seguridad de tipos lo suficiente como para hacer que cada caso de uso se compile, pero eso es ignorar el hecho de que se trata de una solicitud de función y no tan loca.

Sí, obviamente funciona, pero el objetivo de la verificación de tipos es ... verificar tipos.

Este es un hilo largo, largo, largo, sobre cómo el lado estático de la clase es una interfaz separada del lado de la instancia y implements apunta al lado de la instancia. @ andy-ms le estaba indicando a @ tyteen4a03 cómo hacer que un fragmento de código funcionara, porque estaba _ incorrecto_, no renunciar a la verificación de tipos.

Mi caso de uso para permitir que los métodos estáticos usen el parámetro genérico de la clase es para clases mixtas. Estoy construyendo un marco de entidad que usa anotaciones para definir columnas, bases de datos, etc. y me gustaría tener una función estática mezclada con mis clases de entidad que permitiría un acceso conveniente al repositorio escrito correctamente.

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

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

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

@rmblstrp ¿Puede mostrar cómo usaría la función propuesta en su ejemplo? ¿Preferiblemente como algo verificable?

@rmblstrp que no requiere esta función. De hecho, dado que está utilizando un decorador, en realidad es _su tipo_ para verificar que las clases anotadas proporcionen los métodos estáticos necesarios. No necesita ambos y en realidad sería bastante redundante.

Hola, no quiero quedarme fuera de tema o fuera del alcance de esta conversación.
Sin embargo, dado que trajo en discusión diferentes paradigmas de programación (¿deberíamos usar POO o funcional?), Quiero hablar específicamente sobre las fábricas estáticas que se usan generalmente para crear una conexión a una base de datos o proporcionar algún tipo de servicio.
En muchos lenguajes como PHP y Java, las fábricas estáticas han quedado obsoletas en favor de la inyección de dependencia. La inyección de dependencias y los contenedores IOC se han hecho populares gracias a marcos como Symfony y Spring.
Typecript tiene un maravilloso contenedor IOC llamado InversifyJS.

Estos son los archivos donde puede ver cómo se maneja todo.
https://github.com/Deviad/virtual-life/blob/master/models/generic.ts
https://github.com/Deviad/virtual-life/blob/master/service/user.ts
https://github.com/Deviad/virtual-life/blob/master/models/user.ts
https://github.com/Deviad/virtual-life/blob/master/utils/sqldb/client.ts
https://github.com/Deviad/virtual-life/blob/master/bootstrap.ts

No estoy diciendo que sea una solución perfecta (y probablemente deje algunos escenarios descubiertos) pero también funciona con React, hay algunos ejemplos que ya puedes ver.

Además, te sugiero que mires este video sobre programación funcional que cubre este aspecto sobre cuál es mejor: https://www.youtube.com/watch?v=e-5obm1G_FY&t=1487s
SPOILER: nadie es mejor, depende del problema que quieras solucionar. Si se trata de usuarios, aulas, profesores de programación orientada a objetos, será mejor modelar su problema utilizando objetos.
Si necesita un analizador que escanee un sitio web, quizás sea funcional, puede ser mejor usar generadores de funciones que devuelvan resultados parciales, etc. :)

@Deviad @aluanhaddad Desde mi publicación he estado usando InversifyJS y ha sido absolutamente genial y definitivamente una forma mucho mejor de hacerlo. En el momento de mi publicación, acababa de comenzar a usar TypeScript / Node después de haber sido PHP / C # anteriormente. Solo tomó un poco de tiempo familiarizarse con el entorno y los paquetes disponibles.

¿Cuál es el estado de esto? ¿Por qué sigues cerrando problemas no resueltos en el repositorio?

Creo que este es uno de esos casos en los que el problema debe etiquetarse explícitamente con "wontfix", ya que la elección de no tener métodos estáticos en las interfaces es por diseño.

@ enet4 , soy un recién llegado, pero eso no estaba nada claro. Al leer este y otros problemas relacionados, parece que se trata principalmente de los siguientes problemas:

A. es dificil
B. No nos gusta (cada) sintaxis específica que hemos visto hasta ahora.
C. Una pequeña minoría no cree que sea factible en absoluto y prefiere eliminar la extraña forma actual de hacerlo.

Si en realidad es por diseño, y los responsables no lo quieren, deberían escribir un documento público y vincularlo a este y otros hilos. Esto nos ahorraría mucho tiempo en vez de mantenernos en el limbo.

Ya hemos vinculado a # 14600 en este hilo y ese es el problema a seguir para la solicitud de función.

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

Temas relacionados

seanzer picture seanzer  ·  3Comentarios

MartynasZilinskas picture MartynasZilinskas  ·  3Comentarios

Antony-Jones picture Antony-Jones  ·  3Comentarios

fwanicka picture fwanicka  ·  3Comentarios

kyasbal-1994 picture kyasbal-1994  ·  3Comentarios