Typescript: Fügen Sie Support-Schnittstellen hinzu, um statische Methoden zu definieren

Erstellt am 13. Jan. 2017  ·  43Kommentare  ·  Quelle: microsoft/TypeScript

TypeScript-Version: 2.0.3

Code

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

Erwartetes Verhalten:
Keine Fehler
Tatsächliches Verhalten:
Nicht unterstützt

Question

Hilfreichster Kommentar

@aluanhaddad Ich finde Ihren Code eine Problemumgehung für etwas, das offensichtlich ist.

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

Was sehen Sie deutlicher?

Alle 43 Kommentare

Was möchten Sie damit erreichen? Können Sie das Szenario näher erläutern?

Ich stelle mir vor, abstract Klassen sind das, wonach Sie suchen.

@mhegazy
Beispiel:

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

@rozzzly In diesem Beispiel ist abstract class nicht gültig.

Ich denke das wäre ziemlich cool! Java hat diese Funktionalität in der letzten Version hinzugefügt.

@ Serginho
Ich denke, Sie finden das vielleicht interessant:

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 Ich finde Ihren Code eine Problemumgehung für etwas, das offensichtlich ist.

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

Was sehen Sie deutlicher?

@aluanhaddad Ich finde Ihren Code eine Problemumgehung für etwas, das offensichtlich ist.

Schnittstelle JsonSerializable {
public static fromJson (obj: any);
public toJson (): string;
}}
Was sehen Sie deutlicher?

In der Tat, aber meine ist keine Problemumgehung, sondern eine Rechtfertigung für Ihren Anwendungsfall. Statische Deserialisierung und Instanzserialisierung werden klassenweise implementiert und dadurch gekapselt und typsicher.
Ihre Erklärung:

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

ist irrelevant, da es die Schnittstelle des JSON-Objekts grundsätzlich neu deklariert. Das erfordert in keiner Weise statische Schnittstellenmethoden.

Sie verlieren das Konzept der Schnittstelle. class A implements JsonSerializable sollte mich dazu bringen, beide Methoden zu implementieren. Aber eigentlich bringt mich dazu umzusetzen:

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

Es ist keine klare Lösung.

Es gibt keinen technischen Grund, statische Methoden für Schnittstellen nicht zu definieren.

@Serginho Ich behaupte nicht, dass die Situation ideal ist. Ich habe nur versucht zu veranschaulichen, dass es ausgedrückt werden kann.

@aluanhaddad Komm schon! Öffne deinen Geist. Denken Sie, dass Typoskript statische Methoden in Schnittstellen zulassen sollte? Dies wurde in Java 8 in der letzten Version implementiert, also denke ich, ich spreche nicht von Unsinn.

@Serginho Ich denke nicht, dass es besonders gut zu TypeScript passt. Schnittstellen sollten die Funktionalität definieren, die ein Objekt bietet. Diese Funktionalität sollte überschreibbar und austauschbar sein (deshalb sind Schnittstellenmethoden virtuell). Statik ist ein paralleles Konzept zu dynamischem Verhalten / virtuellen Methoden. Das Verweben der beiden fühlt sich aus gestalterischer Sicht für mich nicht richtig an.

Wie @aluanhaddad bereits schrieb, verfügt TypeScript bereits über einen Mechanismus, um auszudrücken, was Sie wünschen. Der große Unterschied besteht darin, dass Sie in diesem Fall die Klasse als Objekt behandeln, das weiterhin logisch konsistent ist. Aus meiner Sicht ist der von Ihnen vorgeschlagene Ansatz für TypeScript (und die JavaScript-Entwicklung) nicht besonders gut geeignet, da Klassen eine Art Hack / Bürger zweiter Klasse sind. Die derzeit vorherrschenden Muster beziehen sich auf Ententypisierung / Formprogrammierung, nicht auf starre Klassen.

Ich denke, Sie können auch Ihren Geist öffnen und verschiedene Programmierstile ausprobieren. Die Welt beginnt und endet nicht mit OOP. Möglicherweise finden Sie die Programmierung mit Funktionen, einfachen Objekten und (und bis zu einem gewissen Grad) Prototypen unterhaltsam und sogar noch besser. Ein solcher Stil entspricht viel mehr dem, wofür JavaScript ursprünglich entwickelt wurde. Nebenbei bemerkt stimme ich zu, dass sich JS langsam in Richtung OOP-ähnlicher Muster bewegt (zumindest auf Komitee-Ebene), aber das liegt an dem enormen Druck der Leute, die mit verschiedenen Programmierparadigmen und Entwicklungstechniken nicht vertraut sind. Für mich ist es eine zutiefst traurige und enttäuschende Sache.

Wie @gcnew sagte, beschreibt eine Schnittstelle die Form eines einzelnen Objekts, nicht seine Klasse. Es gibt jedoch keinen Grund, warum wir keine Schnittstelle verwenden können, um die Form der Klasse selbst zu beschreiben, da Klassen auch Objekte sind.

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

Wenn die Signatur von toJson oder fromJson eingehalten wird, tritt ein Kompilierungsfehler auf. (Leider liegt der Fehler eher bei const _ als bei der Methode.)

Wahrscheinlich verwandt (da es sich um die Eingabe statischer Methoden handelt): # 5863.


@aluanhaddad : Ihr Beispiel hat einen Fehler: JsonSerializable 's constructor -Mitglied verweist tatsächlich auf eine Instanzeigenschaft namens constructor , die beim Aufrufen mit new aufgerufen würde Geben Sie ein JsonSerializableStatic . Das würde bedeuten, dass (new ((new X()).constructor)).fromJson({}) funktionieren müsste. Der Grund , warum es erfolgreich kompiliert ist , dass interface A extends JsonSerializable<typeof A> , die Umsetzung , ohne es wirklich zu überprüfen, gültig erklärt. Dies wird beispielsweise ohne Fehler kompiliert:

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

@Serginho Kein Java-Benutzer, aber es sieht nicht so aus, als Anforderungen zu erfüllen ). Mit Java können Sie eine statische Methode mit einem Body in einer Schnittstelle definieren, deren TypeScript-Äquivalent lautet:

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

Was wäre, wenn Sie versuchen würden, eine generische Fabrik zu bauen?

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

Ich bin mir nicht sicher über die interface -Syntax, die ich hier vorschlage, aber ich weiß, dass ich die "Verwendungs" -Syntax erreichen möchte. Dies ist ein Muster, das ich häufig in Swift verwende (mit protocols ), und ich denke, es wäre in TypeScript wirklich schön. Obwohl ich kein Sprachdesigner oder Compiler-Implementierer bin, bin ich mir nicht sicher, ob es der beabsichtigten Richtung von TypeScript entspricht oder realistisch zu implementieren ist.

Die Klasse muss die Schnittstelle nicht implement . Sie definieren nur die Schnittstelle, und bei der Überprüfung des strukturellen Typs werden alle Probleme am Verwendungsort zwischengespeichert, z.

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 das ist eine relativ gute Lösung. Vielen Dank für die Bereitstellung! 🙏

Es gibt immer noch zwei Dinge, die mir Unbehagen bereiten (zugegebenermaßen sind es auch keine Showstopper):

  1. Wir können immer noch nicht erklären, dass Bar explizit Factorizable implementiert oder diesen entspricht.

    • In der Praxis stelle ich mir vor, dass dies wirklich kein Problem ist. Da es wahr wäre, dass wenn sich die Factorizable -Schnittstelle auf inkompatible Weise ändert, die Verwendung von x.bar(Bar) fehlerhaft wird und Sie dann die Driftänderungen korrigieren würden.
  2. Für mich ist es immer noch eine kognitive Belastung, den Typ von y auf der rechten Seite der Aufgabe anzugeben.

    • Seltsamerweise würde diese Syntax dieses Verhalten aktivieren: var y: Baz[] = x.bar(Bar) . Offensichtlich ein Fehler, aber die Syntax ermöglicht es dem Entwickler, das Problem zu stark einzuschränken, indem der Rückgabetyp an zwei Stellen definiert wird.

Wir können immer noch nicht erklären, dass Bar Factorizable explizit implementiert oder konform ist.

Es gibt zwei Typen: 1. Konstruktorfunktion (z. B. statische Seite der Klasse) und 2. Instanzseite (was beim Aufrufen von new herauskommt). Das Mischen dieser beiden in einem Typ ist nicht korrekt. Theoretisch können Sie implements und static implements aber in der Praxis wird dies, wie Sie bemerkt haben, selten verwendet, und die Implementierungsklausel fügt wirklich nicht viel hinzu. Die Überprüfung wird auf der verwendeten Site auf jede Art und Weise durchgeführt, unabhängig davon, ob Sie eine implements -Klausel haben oder nicht.

Für mich ist es immer noch eine kognitive Belastung, den Typ von y auf der rechten Seite der Aufgabe anzugeben.

Die beiden bedeuten unterschiedliche Dinge: var y = x.bar(Bar) deklariert eine neue Variable y mit dem gleichen Typ wie x.bar(Bar) ; Dabei deklariert var y: Bar[] = x.bar(Bar) eine neue Variable y mit dem Typ Bar[] und überprüft, ob der Typ x.bar(Bar) Bar[] zuweisbar ist.

In Anbetracht dessen ist dies eher ein Stilproblem. Persönlich empfehle ich, Typanmerkungen nicht explizit zu verwenden, es sei denn, Sie müssen; Lassen Sie die Typen durch das System fließen. Ich habe jedoch Codebasen gesehen, in denen der Styleguide das Gegenteil ist, in denen alles eine explizite Typanmerkung hat.

@mhegazy danke für die Diskussion / Perspektive.

@ andy-hanson Danke, dass du dir die Zeit genommen hast, mich zu korrigieren. Ich habe den Fehler in meinem Beispiel behoben.

Dies funktioniert auch und zeigt Fehler beim Kompilieren ohne zusätzlichen Funktionsaufruf an:

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

Was ist das Ergebnis der Diskussion?

Ich bin darauf gestoßen und möchte auch static interface :

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

Die meisten Leute vergessen, dass es bereits statische Schnittstellen in dem Sinne gibt, dass eine Konstruktorfunktion / -klasse bereits zwei Schnittstellen hat, die Konstruktorschnittstelle und die Instanzschnittstelle.

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 */ }

Wenn Sie keine _abstract_-Klassen verwenden, haben Sie bereits die Macht.

Vielen Dank an @kitsonk für die Antwort.

Ihre Erklärung scheint zu funktionieren, ist aber für den Fall zu ausführlich.

Und ich habe gerade _abstract_ Klassen ausprobiert, aber es scheint nicht static mit abstract .

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

@ Zixia , das ist Ausgabe # 14600

Ja, lass es uns abstimmen.

Würde jemand gerne mit einer Lösung auf meine Frage hier antworten: http://stackoverflow.com/questions/44047874/dynamically-modify-typescript-classes-through-a-generic-function

Ich denke, dass das gesamte Problem gelöst werden könnte, indem statische Elemente auf Schnittstellen angegeben werden können. Wenn dieses Problem geschlossen wird, weil es nicht benötigt wird , würde ich sehr gerne sehen, wie es stattdessen gelöst werden kann.

@grantila Ich habe deine Frage beantwortet. Wie bereits in dieser Ausgabe erwähnt, kann dies leicht gelöst werden, indem Klassen als Objekte behandelt werden, es sei denn, Sie haben weitere Anforderungen, die dort nicht erwähnt wurden.

@ Enet4 Ich habe die Frage aktualisiert, sie wurde zu stark vereinfacht. Das eigentliche Problem kann leider nicht durch den Object.defineProperty() Hack behoben werden. Welche btw, ist ein Hack. Ich möchte sicherstellen, dass make nicht falsch geschrieben wurde - im Grunde genommen eine ordnungsgemäße statische Überprüfung.

Dies ist ein echtes Problem, Code, den ich auf TypeScript portiert habe, jetzt aber als JavaScript beibehalten habe, da ich derzeit nicht so große Mengen an Code umschreiben kann, wie es sonst notwendig gewesen wäre.

Ich möchte sicherstellen, dass keine falsch geschriebenen Marken erstellt werden - im Grunde genommen eine ordnungsgemäße statische Überprüfung.

Statische Prüfung kann hier nur so weit gehen. Wenn Sie jetzt jedoch nur Bedenken haben, dass Sie die richtige Eigenschaft festlegen, scheint dies durch Casting in einen Maker-Typ und Festlegen der Eigenschaft von dort aus behoben zu werden.

@ Enet4 das ist eine funktionierende Lösung, danke. Ich denke, dieses Problem (13462) sollte noch einmal betrachtet werden, da Lösungen wie Ihre, die Typ Casting verwenden, tatsächlich nicht typsicher sind, und wenn dies der einzige Weg ist, die Situation zu lösen, mit dem Klassentyp als Wert zu arbeiten, wir verlieren viel von der Flexibilität einer dynamischen Sprache.

Lösungen wie Ihre, die Typumwandlung verwenden, sind eigentlich nicht typsicher. Wenn dies die einzige Möglichkeit ist, die Situation zu lösen, mit dem Klassentyp als Wert zu arbeiten, verlieren wir viel von der Flexibilität einer dynamischen Sprache.

@grantila Zu meiner Verteidigung ist das umstritten. : wink: Ihr Anwendungsfall unterscheidet sich von den in dieser Ausgabe vorgestellten darin, dass Ihr Klassentyp abhängig von den Laufzeitbedingungen eine Methode bereitstellen kann (oder nicht). Und IMO ist unsicherer als die in meiner Antwort dargestellte Typumwandlung, die nur durchgeführt wurde, um das Einfügen eines Feldes in ein Objekt zu ermöglichen. In diesem Sinne sollte der resultierende Klassentyp C & Maker<T> mit allem anderen kompatibel bleiben, das auf einem C .

Ich habe auch versucht mir vorzustellen, wo statische Methoden in Schnittstellen Ihnen hier helfen könnten, aber mir fehlt möglicherweise etwas. Selbst wenn Sie so etwas wie ein static make?(... args: any[]): self in Ihrer Schnittstelle hätten, müsste es vor einem Aufruf zur Laufzeit auf Existenz überprüft werden. Wenn Sie diese Diskussion fortsetzen möchten, sollten Sie dies an anderer Stelle tun, um das Rauschen zu reduzieren. : leicht_smiling_face:

Wir können also keine statischen Factory-Methoden in Klassen eingeben, die dieselbe Schnittstelle implementieren.

Mein Anwendungsfall ist:

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

Ich möchte nicht nur dafür eine neue Fabrikklasse schreiben.

@ tyteen4a03 Entfernen Sie IObject aus diesem Beispiel, und es wird kompiliert. Siehe auch https://github.com/Microsoft/TypeScript/issues/17545#issuecomment -319422545

@ andy-ms Ja, offensichtlich funktioniert es, aber der gesamte Punkt der Typprüfung besteht darin, Typen zu prüfen. Sie können die Typensicherheit immer so weit herabsetzen, dass jeder Anwendungsfall kompiliert werden kann. Dabei wird jedoch die Tatsache ignoriert, dass es sich um eine Funktionsanforderung handelt und eine nicht so verrückte.

Ja, offensichtlich funktioniert es, aber der gesamte Punkt der Typprüfung besteht darin, Typen zu prüfen.

Dies ist ein langer, langer, langer Thread darüber, wie die statische Seite der Klasse eine separate Schnittstelle zur Instanzseite darstellt und implements auf die Instanzseite zeigt. @ andy-ms gab @ tyteen4a03 an, wie ein Laufen gebracht werden soll , weil es falsch war, nicht auf die Typprüfung zu verzichten.

Mein Anwendungsfall, um statischen Methoden die Verwendung des generischen Klassenparameters zu ermöglichen, ist für Mixin-Klassen. Ich erstelle ein Entitätsframework, das Anmerkungen zum Definieren von Spalten, Datenbanken usw. verwendet, und möchte eine statische Funktion in meine Entitätsklassen einmischen, die einen bequemen Zugriff auf das korrekt typisierte Repository ermöglicht.

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 Können Sie zeigen, wie Sie die vorgeschlagene Funktion in Ihrem Beispiel verwenden würden? Am liebsten als etwas Überprüfbares?

@rmblstrp , für das diese Funktion nicht erforderlich ist. Da Sie einen Dekorator verwenden, können Sie tatsächlich _its type_ verwenden, um zu überprüfen, ob die mit Anmerkungen versehenen Klassen die erforderlichen statischen Methoden bereitstellen. Sie brauchen nicht beides und es wäre eigentlich ziemlich überflüssig.

Hallo, ich möchte nicht aus dem Thema oder dem Rahmen dieses Gesprächs herauskommen.
Da Sie jedoch verschiedene Programmierparadigmen zur Diskussion gestellt haben (sollten wir OOP oder funktionale verwenden?), Möchte ich speziell auf statische Fabriken eingehen, die normalerweise verwendet werden, um eine Verbindung zu einer Datenbank herzustellen oder eine Art Dienst bereitzustellen.
In vielen Sprachen wie PHP und Java waren statische Fabriken zugunsten von Dependency Injection veraltet. Dependency Injection- und IOC-Container wurden dank Frameworks wie Symfony und Spring populär gemacht.
Typescript hat einen wunderbaren IOC-Container namens InversifyJS.

In diesen Dateien können Sie sehen, wie das Ganze gehandhabt wird.
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

Ich sage nicht, dass es eine perfekte Lösung ist (und wahrscheinlich bleiben einige Szenarien offen), aber es funktioniert auch mit React. Es gibt bereits einige Beispiele, die Sie sich ansehen können.

Außerdem empfehle ich Ihnen, sich dieses Video über funktionale Programmierung anzusehen, das diesen Aspekt abdeckt, der besser ist: https://www.youtube.com/watch?v=e-5obm1G_FY&t=1487s
SPOILER: Niemand ist besser, es hängt von dem Problem ab, das Sie lösen möchten. Wenn Sie sich mit Benutzern, Klassenzimmern und Lehrern beschäftigen, ist OOP besser geeignet, Ihr Problem mithilfe von Objekten zu modellieren.
Wenn Sie einen Parser benötigen, der eine Website scannt, ist es möglicherweise besser, Funktionsgeneratoren zu verwenden, die Teilergebnisse usw. zurückgeben :) :)

@ Deviad @aluanhaddad Seit meinem Beitrag

Wie ist der Status davon? Warum schließen Sie immer wieder ungelöste Probleme im Repo?

Ich glaube, dies ist einer der Fälle, in denen das Problem explizit mit "wontfix" gekennzeichnet werden sollte, da die Entscheidung, keine statischen Methoden in Schnittstellen zu haben, beabsichtigt ist.

@ enet4 , ich bin ein Neuling, aber das war überhaupt nicht klar. Wenn man dieses und andere verwandte Themen liest, hört es sich meistens nach den folgenden Themen an:

A. Es ist schwer
B. Wir mögen nicht (jede) spezifische Syntax, die wir bisher gesehen haben.
C. Eine kleine Minderheit glaubt nicht, dass dies überhaupt machbar sein sollte, und würde lieber die derzeitige seltsame Art, dies zu tun, löschen.

Wenn es tatsächlich beabsichtigt ist und die Verantwortlichen es nicht wollen, sollten sie ein öffentliches Dokument schreiben und es mit diesem und anderen Threads verknüpfen. Dies würde uns viel Zeit sparen, um uns in der Schwebe zu halten.

Wir haben in diesem Thread bereits einen Link zu # 14600 erstellt. Dies ist das Problem, das bei der Funktionsanforderung zu beachten ist.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen