Typescript: Vorschlag: Fügen Sie abstrakte statische Methoden in Klassen und statische Methoden in Schnittstellen hinzu

Erstellt am 12. März 2017  ·  98Kommentare  ·  Quelle: microsoft/TypeScript

Als Fortsetzungsproblem #2947, das den Modifikator abstract bei Methodendeklarationen zulässt, aber bei statischen Methodendeklarationen nicht zulässt, schlage ich vor, diese Funktionalität auf statische Methodendeklarationen zu erweitern, indem der Modifikator abstract static bei Methodendeklarationen zugelassen wird .

Das damit verbundene Problem betrifft den Modifikator static bei der Deklaration von Schnittstellenmethoden, der nicht zulässig ist.

1. Das Problem

1.1. Abstrakte statische Methoden in abstrakten Klassen

In einigen Fällen der Verwendung der abstrakten Klasse und ihrer Implementierungen muss ich möglicherweise einige klassenabhängige (nicht instanzabhängige) Werte haben, auf die im Kontext der untergeordneten Klasse (nicht im Kontext eines Objekts) zugegriffen werden sollte ein Objekt erstellen. Die Funktion, die dies ermöglicht, ist der Modifikator static in der Methodendeklaration.

Zum Beispiel (Beispiel 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'

Aber in einigen Fällen muss ich auch auf diesen Wert zugreifen, wenn ich nur weiß, dass die zugreifende Klasse von AbstractParentClass geerbt ist, ich aber nicht weiß, auf welche spezifische untergeordnete Klasse ich zugreife. Ich möchte also sicher sein, dass jedes Kind der AbstractParentClass diese statische Methode hat.

Zum Beispiel (Beispiel 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'.
    }
}

Infolgedessen entscheidet der Compiler, dass ein Fehler aufgetreten ist, und zeigt die folgende Meldung an: Property 'getSomeClassDependentValue' does not exist on type 'typeof AbstractParentClass'.

1.2. Statische Methoden in Schnittstellen

In einigen Fällen impliziert die Schnittstellenlogik, dass die implementierenden Klassen eine statische Methode haben müssen, die die vorbestimmte Liste von Parametern hat und den Wert des exakten Typs zurückgibt.

Zum Beispiel (Beispiel 3):

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

Beim Kompilieren dieses Codes tritt ein Fehler auf: Der Modifikator „static“ kann nicht auf einem Typmember erscheinen.

2. Die Lösung

Die Lösung für beide Probleme (1.1 und 1.2) besteht darin, den Modifikator abstract bei statischen Methodendeklarationen in abstrakten Klassen und den Modifikator static in Schnittstellen zuzulassen.

3. JS-Implementierung

Die Implementierung dieser Funktion in JavaScript sollte der Implementierung von Schnittstellen, abstrakten Methoden und statischen Methoden ähneln.

Dies bedeutet, dass:

  1. Das Deklarieren abstrakter statischer Methoden in einer abstrakten Klasse sollte sich nicht auf die Darstellung der abstrakten Klasse im JavaScript-Code auswirken.
  2. Das Deklarieren statischer Methoden in der Schnittstelle sollte die Darstellung der Schnittstelle im JavaScript-Code nicht beeinflussen (sie ist nicht vorhanden).

Zum Beispiel dieser TypeScript-Code (Beispiel 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';
    }
}

sollte zu diesem JS-Code kompiliert werden:

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. Relevante Punkte

  • Die Deklaration einer abstrakten statischen Methode einer abstrakten Klasse sollte mit dem Modifikator abstract static gekennzeichnet werden
  • Die Implementierung einer abstrakten statischen Methode einer abstrakten Klasse sollte mit dem Modifikator abstract static oder static gekennzeichnet sein
  • Die Deklaration der statischen Methode einer Schnittstelle sollte mit dem Modifikator static gekennzeichnet sein
  • Die Implementierung der statischen Methode einer Schnittstelle sollte mit dem Modifikator static gekennzeichnet werden

Alle anderen Eigenschaften des Modifikators abstract static sollten von den Eigenschaften der Modifikatoren abstract und static geerbt werden.

Alle anderen Eigenschaften des Modifikators static der Schnittstellenmethode sollten von den Eigenschaften der Schnittstellenmethoden und der Modifikatoren static geerbt werden.

5. Checkliste für Sprachmerkmale

  • Syntaktisch

    • _Was ist die Grammatik dieser Funktion?_ - Die Grammatik dieser Funktion ist abstract static Modifikator einer abstrakten Klassenmethode und static Modifikator einer Schnittstellenmethode.

    • _Gibt es Auswirkungen auf die JavaScript-Abwärtskompatibilität? Wenn ja, werden sie ausreichend gemildert?_ - Es gibt keine Auswirkungen auf die JavaScript-Abwärtskompatibilität.

    • _Beeinträchtigt diese Syntax ES6 oder plausible ES7-Änderungen?_ - Diese Syntax beeinträchtigt ES6 oder plausible ES7-Änderungen nicht.

  • Semantik

    • _Was ist ein Fehler unter der vorgeschlagenen Funktion?_ - Jetzt gibt es Fehler beim Kompilieren des abstract static -Modifikators einer abstrakten Klassenmethode und des static -Modifikators einer Schnittstellenmethode.

    • _Wie wirkt sich das Merkmal auf Untertyp-, Obertyp-, Identitäts- und Zuweisbarkeitsbeziehungen aus?_ - Das Merkmal wirkt sich nicht auf Untertyp-, Obertyp-, Identitäts- und Zuweisbarkeitsbeziehungen aus.

    • _Wie interagiert die Funktion mit Generika?_ - Die Funktion interagiert nicht mit Generika.

  • Emittieren

    • _Welche Auswirkungen hat diese Funktion auf die JavaScript-Ausgabe?_ - Diese Funktion hat keine Auswirkungen auf die JavaScript-Ausgabe.

    • _Wird dies bei Vorhandensein von Variablen des Typs 'beliebig' korrekt ausgegeben?_ - Ja.

    • _Was sind die Auswirkungen auf die Deklarationsdatei (.d.ts)?_ - Es gibt keine Auswirkungen auf die Deklarationsdatei.

    • _Spielt diese Funktion gut mit externen Modulen?_ - Ja.

  • Kompatibilität

    • _Ist dies eine bahnbrechende Änderung gegenüber dem 1.0-Compiler?_ - Wahrscheinlich ja, der 1.0-Compiler wird den Code, der diese Funktion implementiert, nicht kompilieren können.

    • _Ist dies eine grundlegende Änderung des JavaScript-Verhaltens?_ - Nein.

    • _Ist dies eine inkompatible Implementierung einer zukünftigen JavaScript-Funktion (z. B. ES6/ES7/später)?_ - Nein.

  • Andere

    • _Kann das Feature implementiert werden, ohne die Compilerleistung negativ zu beeinflussen?_ - Wahrscheinlich ja.

    • _Welche Auswirkungen hat es auf Tooling-Szenarien, wie z. B. Member Completion und Signaturhilfe in Editoren?_ - Wahrscheinlich hat es keine Auswirkungen dieser Art.

Awaiting More Feedback Suggestion

Hilfreichster Kommentar

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

Ich möchte die Implementierung der statischen Deserialisierungsmethode in den Unterklassen von Serializable erzwingen.
Gibt es eine Problemumgehung, um ein solches Verhalten zu implementieren?

Alle 98 Kommentare

Statische Schnittstellenmethoden sind im Allgemeinen nicht sinnvoll, siehe #13462

Warte damit, bis wir mehr Feedback dazu hören.

Eine wichtige Frage, die wir hatten, als wir darüber nachdachten: Wer darf eine abstract static -Methode aufrufen? Vermutlich können Sie AbstractParentClass.getSomeClassDependentValue nicht direkt aufrufen. Aber können Sie die Methode für einen Ausdruck vom Typ AbstractParentClass aufrufen? Wenn ja, warum sollte das erlaubt sein? Wenn nicht, was nützt die Funktion?

Statische Schnittstellenmethoden sind im Allgemeinen nicht sinnvoll, siehe #13462

In der Diskussion von #13462 habe ich nicht gesehen, warum Schnittstellenmethoden static sinnlos sind. Ich habe nur gesehen, dass ihre Funktionalität auf andere Weise implementiert werden kann (was beweist, dass sie nicht sinnlos sind).

Aus praktischer Sicht ist die Schnittstelle eine Art Spezifikation - eine bestimmte Menge von Methoden, die für ihre Implementierung in einer Klasse, die diese Schnittstelle implementiert, obligatorisch sind. Die Schnittstelle definiert nicht nur die Funktionalität, die ein Objekt bereitstellt, sie ist auch eine Art Vertrag.

Ich sehe also keine logischen Gründe, warum class die Methode static haben könnte und interface nicht.

Wenn wir dem Standpunkt folgen, dass alles, was bereits in der Sprache implementiert werden kann, keiner Verbesserungen der Syntax und anderer Dinge bedarf (dieses Argument war nämlich einer der Hauptpunkte in der Diskussion von #13462), dann lassen Sie sich davon leiten Aus dieser Sicht können wir entscheiden, dass der Zyklus while redundant ist, da er mit for und if zusammen implementiert werden kann. Aber wir werden while nicht abschaffen.

Eine wichtige Frage, die wir hatten, als wir darüber nachdachten: Wer darf eine abstract static -Methode aufrufen? Vermutlich können Sie AbstractParentClass.getSomeClassDependentValue nicht direkt aufrufen. Aber können Sie die Methode für einen Ausdruck vom Typ AbstractParentClass aufrufen? Wenn ja, warum sollte das erlaubt sein? Wenn nicht, was nützt die Funktion?

Gute Frage. Könnten Sie, da Sie dieses Problem in Betracht gezogen haben, bitte Ihre Ideen dazu mitteilen?

Mir fällt nur ein, dass auf Compiler-Ebene der Fall des direkten Aufrufs von AbstractParentClass.getSomeClassDependentValue nicht verfolgt wird (weil er nicht verfolgt werden kann) und der JS-Laufzeitfehler auftritt. Aber ich bin mir nicht sicher, ob dies mit der TypeScript-Ideologie vereinbar ist.

Ich habe nur gesehen, dass ihre Funktionalität auf andere Weise implementiert werden kann (was beweist, dass sie nicht sinnlos sind).

Nur weil etwas umsetzbar ist, heißt es noch lange nicht, dass es Sinn macht. 😉

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

Ich möchte die Implementierung der statischen Deserialisierungsmethode in den Unterklassen von Serializable erzwingen.
Gibt es eine Problemumgehung, um ein solches Verhalten zu implementieren?

Was ist die neueste Meinung dazu? Ich habe gerade versucht, eine abstrakte statische Eigenschaft einer abstrakten Klasse zu schreiben, und war wirklich überrascht, als es nicht erlaubt war.

Was @patryk-zielinski93 gesagt hat. Da wir aus einigen Jahren von Projekten in PHP kommen, die wir in TS konvertieren, wollen wir statische Methoden in Schnittstellen.

Ein sehr häufiger Anwendungsfall, bei dem dies hilfreich ist, sind React-Komponenten, bei denen es sich um Klassen mit statischen Eigenschaften wie displayName , propTypes und defaultProps handelt.

Aufgrund dieser Einschränkung umfassen die Typisierungen für React derzeit zwei Typen: eine Component -Klasse und ComponentClass -Schnittstelle einschließlich der Konstruktorfunktion und statischen Eigenschaften.

Um eine React-Komponente mit all ihren statischen Eigenschaften vollständig zu überprüfen, muss man beide Typen verwenden.

Beispiel ohne ComponentClass : Statische Eigenschaften werden ignoriert

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

Beispiel mit ComponentClass : Statische Eigenschaften werden typgeprüft

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

Ich vermute, dass viele Leute derzeit ComponentClass nicht verwenden, ohne zu wissen, dass ihre statischen Eigenschaften nicht typgeprüft werden.

Verwandtes Problem: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16967

Gibt es diesbezüglich Fortschritte? Oder eine Problemumgehung für Konstruktoren für abstrakte Klassen?
Hier ist ein Beispiel:

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!

Ich bin mir einig, dass dies eine äußerst leistungsstarke und nützliche Funktion ist. Meiner Ansicht nach macht dieses Merkmal Klassen zu „Bürgern erster Klasse“. Die Vererbung von Klassen/statischen Methoden kann und wird sinnvoll sein, insbesondere für das statische Methodenfabrikmuster, das hier von anderen Postern mehrfach aufgerufen wurde. Dieses Muster ist besonders nützlich für die Deserialisierung, eine häufig durchgeführte Operation in TypeScript. Beispielsweise ist es durchaus sinnvoll, eine Schnittstelle definieren zu wollen, die einen Vertrag bereitstellt, der besagt, dass alle implementierenden Typen von JSON instanziierbar sind.

Wenn abstrakte statische Factory-Methoden nicht zugelassen werden, muss der Implementierer stattdessen abstrakte Factory-Klassen erstellen, wodurch unnötigerweise die Anzahl der Klassendefinitionen verdoppelt wird. Und wie andere Poster bereits betont haben, ist dies eine leistungsstarke und erfolgreiche Funktion, die in anderen Sprachen wie PHP und Python implementiert ist.

Neu bei Typescript, aber ich bin auch überrascht, dass dies standardmäßig nicht erlaubt ist und dass so viele Leute versuchen zu rechtfertigen, das Feature nicht hinzuzufügen:

  1. TS benötigt die Funktion nicht, da Sie das, was Sie zu tun versuchen, immer noch mit anderen Mitteln erreichen können (was nur ein gültiges Argument ist, wenn Sie ein Beispiel für eine sehr objektiv bessere Möglichkeit liefern, etwas zu tun, von dem ich gesehen habe sehr wenig)
  2. Nur weil wir es können, heißt das nicht, dass wir es sollten. Großartig: Aber die Leute posten konkrete Beispiele dafür, wie es nützlich/vorteilhaft wäre. Ich verstehe nicht, wie es schaden könnte, es zuzulassen.

Ein weiterer einfacher Anwendungsfall: (idealer Weg, der nicht funktioniert)

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

Stattdessen legt die Listeninstanz den Instanztyp direkt offen, der für die Listeninstanz selbst nicht relevant ist und auf den über den Konstruktor zugegriffen werden sollte. Fühlt sich schmutzig an. Würde sich viel schmutziger anfühlen, wenn wir über ein Datensatzmodell sprechen würden, in dem eine Reihe von Standardwerten über die gleiche Art von Mechanismus usw. angegeben wird.

Ich war wirklich aufgeregt, echte abstrakte Klassen in einer Sprache zu verwenden, nachdem ich so lange in Ruby/Javascript gearbeitet hatte, nur um am Ende bestürzt über die Implementierungsbeschränkungen zu sein - Das obige Beispiel war nur mein erstes Beispiel dafür, obwohl mir viele einfallen andere Anwendungsfälle, in denen es nützlich wäre. Hauptsächlich nur als Mittel zum Erstellen einfacher DSLs/Konfigurationen als Teil der statischen Schnittstelle, indem sichergestellt wird, dass eine Klasse ein Standardwertobjekt oder was auch immer es sein mag, angibt. - Und Sie denken vielleicht, dafür sind Schnittstellen da. Aber für so etwas Einfaches macht es keinen Sinn und führt nur dazu, dass die Dinge komplizierter sind, als sie sein müssen (die Unterklasse müsste die abstrakte Klasse erweitern und eine Schnittstelle implementieren, was die Benennung komplizierter macht, etc).

Ich hatte diese ähnliche Anforderung für mein Projekt zwei Mal. Beide waren verwandt, um zu gewährleisten, dass alle Unterklassen konkrete Implementierungen eines Satzes statischer Methoden bereitstellen. Mein Szenario ist unten beschrieben:

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

Hoffe, das kann meine Anforderungen erklären. Danke.

Für alle, die nach einer Problemumgehung suchen, können Sie diesen Decorator verwenden:

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}}) => {

Da wir über einen Typparameter aufrufen, wird die eigentliche Basisklasse mit ihrer hypothetischen Factory-Methode abstract static niemals involviert sein. Die Typen der Unterklassen stehen strukturell in Beziehung, wenn der Typparameter instanziiert wird.

Was ich in dieser Diskussion nicht sehe, sind Fälle, in denen die Implementierung über den Typ der Basisklasse aufgerufen wird, im Gegensatz zu einem synthetisierten Typ.

Es ist auch wichtig zu bedenken, dass im Gegensatz zu Sprachen wie C#, wo abstract -Member tatsächlich von ihren Implementierungen in abgeleiteten Klassen _überschrieben_ werden, JavaScript-Member-Implementierungen, Instanzen oder andere, niemals geerbte Zahlen überschreiben, sondern sie eher _beschatten_.

Meine 5 Cent hier aus Benutzersicht sind:

Für den Schnittstellenfall sollte der Modifizierer _static_ erlaubt sein

Schnittstellen definieren Verträge, die von implementierenden Klassen erfüllt werden müssen. Schnittstellen sind also Abstraktionen auf einer höheren Ebene als Klassen.

Im Moment sehe ich, dass Klassen ausdrucksstärker sein können als Schnittstellen, in einer Weise, dass wir statische Methoden in Klassen haben können, aber wir können das nicht aus der Vertragsdefinition selbst erzwingen.

Meiner Meinung nach fühlt sich das falsch und hinderlich an.

Gibt es einen technischen Hintergrund, der die Implementierung dieser Sprachfunktion schwierig oder unmöglich macht?

Prost

Schnittstellen definieren Verträge, die von implementierenden Klassen erfüllt werden müssen. Schnittstellen sind also Abstraktionen auf einer höheren Ebene als Klassen.

Klassen haben zwei Schnittstellen, zwei Implementierungsverträge, und daran führt kein Weg vorbei.
Es gibt Gründe, warum Sprachen wie C# auch keine statischen Member für Schnittstellen haben. Schnittstellen sind logischerweise die öffentliche Oberfläche eines Objekts. Eine Schnittstelle beschreibt die öffentliche Oberfläche eines Objekts. Das heißt, es enthält nichts, was nicht da ist. Auf Instanzen von Klassen sind keine statischen Methoden auf der Instanz vorhanden. Sie existieren nur in der Klassen-/Konstruktorfunktion, daher sollten sie nur in dieser Schnittstelle beschrieben werden.

Auf Instanzen von Klassen sind keine statischen Methoden auf der Instanz vorhanden.

kannst du das näher erläutern? Das ist nicht C#

Sie existieren nur in der Klassen-/Konstruktorfunktion, daher sollten sie nur in dieser Schnittstelle beschrieben werden.

Dass wir es haben und es ist, was wir ändern wollen.

Schnittstellen definieren Verträge, die von implementierenden Klassen erfüllt werden müssen. Schnittstellen sind also Abstraktionen auf einer höheren Ebene als Klassen.

Dem stimme ich voll und ganz zu. Siehe das Json-Schnittstellenbeispiel

Hallo @kitsonk , könnten Sie bitte näher darauf eingehen:

Klassen haben zwei Schnittstellen, zwei Implementierungsverträge, und daran führt kein Weg vorbei.

Ich habe diesen Teil nicht verstanden.

Schnittstellen sind logischerweise die öffentliche Oberfläche eines Objekts. Eine Schnittstelle beschreibt die öffentliche Oberfläche eines Objekts. Das heißt, es enthält nichts, was nicht da ist.

Ich stimme zu. Ich sehe keinen Widerspruch zu dem, was ich gesagt habe. Ich habe sogar noch mehr gesagt. Ich sagte, eine Schnittstelle ist ein Vertrag für eine zu erfüllende Klasse .

Auf Instanzen von Klassen sind keine statischen Methoden auf der Instanz vorhanden. Sie existieren nur in der Klassen-/Konstruktorfunktion, daher sollten sie nur in dieser Schnittstelle beschrieben werden.

Ich bin mir nicht sicher, ob ich das richtig verstanden habe, es ist klar, was es sagt, aber nicht, warum Sie es sagen. Ich kann erklären, warum ich meine Aussage immer noch für gültig halte. Ich übergebe Schnittstellen ständig als Parameter an meine Methoden, das heißt, ich habe Zugriff auf Schnittstellenmethoden. Bitte beachten Sie, dass ich Schnittstellen hier nicht verwende, um Datenstrukturen zu definieren, sondern um konkrete Objekte zu definieren, die an anderer Stelle erstellt/hydratisiert werden . Also wenn ich habe:

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

In diesem Methodenkörper kann ich tatsächlich account -Methoden verwenden. Was ich gerne tun könnte, ist, statische Methoden von SalesRepresentativeInterface verwenden zu können, die bereits erzwungen wurden, um in jeder Klasse implementiert zu werden, die ich in account . Vielleicht habe ich eine sehr vereinfachte oder völlig falsche Vorstellung davon, wie man die Funktion verwendet.

Ich denke, dass ich durch das Zulassen des static -Modifikators Folgendes tun kann: SalesRepresentativeInterface.staticMethodCall()

Liege ich falsch ?

Prost

@davidmpaz : Ihre Syntax ist nicht ganz richtig, aber nah dran. Hier ist ein Beispiel für ein Nutzungsmuster:

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

Damit dies funktioniert, benötigen wir leider zwei Features von TypeScript: (1) statische Methoden in Schnittstellen und abstrakte statische Methoden; (2) die Fähigkeit, typeof als Typhinweis mit generischen Klassen zu verwenden.

@davidmpaz

Ich habe diesen Teil nicht verstanden.

Klassen haben zwei Schnittstellen. Die Konstruktorfunktion und der Instanzprototyp. Das Schlüsselwort class ist dafür im Wesentlichen Zucker.

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

kannst du das näher erläutern? Das ist nicht C#

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

const foo = new Foo();

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

Es gibt kein .baz() in Instanzen von Foo . .baz() existiert nur im Konstruktor.

Aus Typperspektive können Sie diese beiden Schnittstellen referenzieren/extrahieren. Foo bezieht sich auf die öffentliche Instanzschnittstelle und typeof Foo bezieht sich auf die öffentliche Konstruktorschnittstelle (die die statischen Methoden enthalten würde).

Um die Erklärung von @kitsonk weiterzuverfolgen , ist hier das obige Beispiel umgeschrieben, um das zu erreichen, was Sie wollen:

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

Beachten Sie hier Folgendes:

  • Die Klausel implements wird nicht wirklich benötigt, jedes Mal, wenn Sie die Klasse verwenden, wird eine Strukturprüfung durchgeführt und die Klasse wird validiert, um mit der erforderlichen Schnittstelle übereinzustimmen.
  • Sie brauchen typeof nicht, um den Konstruktortyp zu erhalten, stellen Sie einfach sicher, dass Ihr T das ist, was Sie erfassen möchten

@mhegazy : Sie mussten meinen toJSON -Aufruf in der JSONSerializable-Schnittstelle entfernen. Obwohl meine einfache Beispielfunktion makeInstance nur fromJSON aufruft, ist es sehr üblich, ein Objekt zu instanziieren und es dann zu verwenden. Sie haben mir die Möglichkeit entzogen, Aufrufe zu tätigen, die von makeInstance zurückgegeben werden, weil ich nicht wirklich weiß, was T in makeInstance ist (besonders relevant, wenn makeInstance ist eine Klassenmethode, die intern verwendet wird, um eine Instanz zu erstellen und sie dann zu verwenden). Das könnte ich natürlich machen:

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

Und jetzt weiß ich, dass mein T alle Methoden haben wird, die auf JSONSerializer verfügbar sind. Aber das ist zu ausführlich und schwer zu erklären (warte, du übergibst ImplementsJSONSerializer , aber das ist kein JSONSerializable , oder? Warte, du schreibst Enten??) . Die einfacher zu begründende Version ist:

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

Aber wie ich in einem früheren Kommentar erwähnt habe, müssen wir jetzt für jede Klasse, die wir instanziieren möchten, eine Factory-Klasse erstellen, die noch ausführlicher ist als das Duck-Typing-Beispiel. Das ist sicherlich ein praktikables Muster, das die ganze Zeit in Java ausgeführt wird (und ich nehme auch C # an). Aber es ist unnötig ausführlich und doppelt. All diese Boilerplates verschwinden, wenn statische Methoden in Schnittstellen erlaubt sind und abstrakte statische Methoden in abstrakten Klassen erlaubt sind.

keine Notwendigkeit für die zusätzliche Klasse. Klassen können static Mitglieder haben. Sie brauchen nur die implements Klausel nicht. in einem nominellen Typsystem brauchen Sie es wirklich, um die "ist ein"-Beziehung für Ihre Klasse zu bestätigen. in einem strukturellen Typsystem braucht man das nicht wirklich. Die Beziehung "ist ein" wird sowieso bei jeder Verwendung überprüft, sodass Sie die implements -Klausel getrost fallen lassen können und die Sicherheit nicht verlieren.

Mit anderen Worten, Sie können eine Klasse haben, die ein statisches fromJSON hat und deren Instanzen ein toJSON haben:

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 Ich habe darauf bereits als praktikable Option hingewiesen. Siehe mein erstes 'Duck-Typing'-Beispiel. Ich argumentierte, dass es unnötig ausführlich und schwer zu begründen sei. Ich habe dann behauptet, dass die Version, über die einfacher zu argumentieren ist (Factory-Klassen), noch ausführlicher ist.

Niemand bestreitet, dass es möglich ist, das Fehlen statischer Methoden in Schnittstellen zu umgehen. Tatsächlich würde ich argumentieren, dass es eine elegantere Problemumgehung gibt, indem funktionale Stile anstelle von Fabrikklassen verwendet werden, obwohl dies meine persönliche Präferenz ist.

Aber meiner Meinung nach ist der sauberste und am einfachsten zu begründende Weg, dieses außergewöhnlich häufige Muster zu implementieren, die Verwendung abstrakter statischer Methoden. Diejenigen von uns, die diese Funktion in anderen Sprachen (Python, PHP) lieben gelernt haben, vermissen sie in TypeScript. Vielen Dank für Ihre Zeit.

Ich habe dann behauptet, dass die Version, über die leichter zu argumentieren ist (Fabrikklassen)

Ich bin mir nicht sicher, ob ich damit einverstanden bin, dass dies einfacher zu begründen ist.

Aber meiner Meinung nach ist der sauberste und am einfachsten zu begründende Weg, dieses außergewöhnlich häufige Muster zu implementieren, die Verwendung abstrakter statischer Methoden. Diejenigen von uns, die diese Funktion in anderen Sprachen (Python, PHP) lieben gelernt haben, vermissen sie in TypeScript.

Und dieses Problem verfolgt das Hinzufügen. Ich wollte nur sicherstellen, dass zukünftige Besucher dieses Threads verstehen, dass dies heute ohne eine implements -Klausel machbar ist.

@kitsonk richtig, das habe ich verpasst.

Leute, also habe ich wie LocationModule, das eine Methode zum Abrufen von Optionen zum Speichern in der Datenbank haben sollte, aus der es sich selbst neu erstellen sollte.
Jedes LocationModule hat seine eigenen Optionen mit Typ für die Erholung.

Also muss ich jetzt eine Fabrik mit Generika zur Erholung erstellen und selbst dann habe ich keine Überprüfung der Kompilierzeit. Weißt du, den Zweck hier zu schlagen.

Zuerst dachte ich: "Nun, keine Statik für Schnittstellen, also bleiben wir bei der abstrakten Klasse, ABER selbst das wäre schmutzig, da ich jetzt überall in meinem Codetyp von der Schnittstelle zur abstrakten Klasse wechseln muss, damit der Prüfer die Methoden erkennt, nach denen ich suche .

Hier:

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

Und dann stolpere ich über gut, statisch kann nicht mit abstrakt sein. Und es kann nicht in der Schnittstelle sein.

Leute, im Ernst, schützen wir uns nicht davor, dies nicht zu implementieren, nur dafür, dass wir dies nicht implementieren?

Ich liebe TypeScript leidenschaftlich. Wie verrückt meine ich. Aber es gibt immer aber.

Anstatt eine erzwungene Implementierung der statischen Methode zu haben, müsste ich es noch einmal überprüfen, wie ich es mit allem in einfachem altem JavaScript tat.

Die Architektur ist bereits voll von Mustern, coolen Entscheidungen usw., aber einige von ihnen dienen nur dazu, Probleme mit der Verwendung so einfacher Dinge wie der statischen Methode der Schnittstelle zu überwinden.

Meine Fabrik muss zur Laufzeit die Existenz statischer Methoden überprüfen. Wie ist das?

Oder noch besser:

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
  dontForgetToHaveStaticMethodForRecreation();
}

Wenn Sie das Klassenobjekt polymorph verwenden, was der einzige Grund dafür ist, eine Schnittstelle zu implementieren, spielt es keine Rolle, ob die Schnittstelle(n), die die Anforderungen für die statische Seite der Klasse spezifizieren, syntaktisch von der Klasse selbst referenziert wird weil es an der Verwendungsstelle überprüft wird und einen Entwurfszeitfehler ausgibt, wenn die erforderliche(n) Schnittstelle(n) nicht implementiert ist/sind.

@malinakirn

Siehe mein erstes 'Duck-Typing'-Beispiel.

Dieses Problem hat keinen Einfluss darauf, ob Duck-Typing verwendet wird oder nicht. TypeScript ist Ententyp.

@aluanhaddad Ihre Implikation ist nicht korrekt. Ich kann Ihren Standpunkt verstehen, dass die primäre Funktion implements ISomeInterface für die polymorphe Verwendung des Klassenobjekts dient. Es kann jedoch wichtig sein, ob die Klasse auf die Schnittstelle(n) verweist, die die statische Form des Klassenobjekts beschreiben.

Sie implizieren, dass implizite Typrückschlüsse und Nutzungsseitenfehler alle Anwendungsfälle abdecken.
Betrachten Sie dieses Beispiel:

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

Im obigen Beispiel ist eine explizite Typdeklaration erforderlich; das Nichtbeschreiben der Form des Klassenobjekts (statische Seite) führt zu einem Entwurfszeitfehler an der Verwendungsstelle, obwohl die tatsächlichen Werte der erwarteten Form entsprechen.

Darüber hinaus lässt uns das Fehlen einer Möglichkeit, auf die Schnittstelle(n) zu verweisen, die die statische Seite beschreiben, die einzige Option, den Typ explizit für jedes einzelne Mitglied zu deklarieren; und das dann in jeder Klasse zu wiederholen.

Aufbauend auf meinem vorherigen Beitrag bin ich der Meinung, dass der Modifikator static in Schnittstellen eine wirklich gute Lösung für einige Anwendungsfälle ist, die gelöst werden müssen. Ich entschuldige mich dafür, wie groß dieser Kommentar geworden ist, aber es gibt eine Menge, die ich veranschaulichen möchte.

1) Es gibt Zeiten, in denen wir in der Lage sein müssen, die Form der statischen Seite eines Klassenobjekts explizit zu beschreiben, wie in meinem obigen Beispiel.
2) Es gibt Zeiten, in denen die Formprüfung an der Definitionssite sehr wünschenswert ist, und Zeiten wie im Beispiel von @OliverJAsh , in denen sich die Verwendungssite in externem Code befindet, der nicht überprüft wird, und Sie eine Formprüfung an der Definitionssite durchführen müssen .

In Bezug auf Nummer 2 schlagen viele Beiträge, die ich gelesen habe, eine Formprüfung vor, da die Verwendungsstelle mehr als ausreichend ist. Aber in Fällen, in denen sich der Verwendungsort in einem weit, weit entfernten Galaxienmodul befindet ... oder wenn sich der Verwendungsort an einem externen Ort befindet, der nicht überprüft wird, ist dies offensichtlich nicht der Fall.

Andere Beiträge schlagen vor, #workarounds auf der Definitionsseite zu überprüfen. Während diese gefürchteten Problemumgehungen es Ihnen ermöglichen, die Form an der Definitionsseite (in einigen Fällen) zu überprüfen, gibt es Probleme:

  • Sie lösen Nummer 1 oben nicht, Sie müssten den Typ immer noch explizit für jedes Mitglied in jeder Klasse deklarieren.
  • In vielen Fällen fehlt es ihnen an Klarheit, Eleganz und Lesbarkeit. Sie sind nicht immer einfach zu bedienen, nicht intuitiv und es kann eine ganze Weile dauern, bis man herausfindet, dass sie möglich sind.
  • Sie funktionieren nicht in allen Fällen und das Finden #workaroundsToMakeTheWorkaroundsWork macht keinen Spaß ... oder produktiv ... aber meistens keinen Spaß.

Hier ist ein Beispiel für die Problemumgehung, die ich in letzter Zeit gesehen habe, um die Formprüfung auf der Definitionsseite zu erzwingen ...

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

Versuchen Sie nun, diese Problemumgehung mit einer eingefügten abstrakten Klasse zu verwenden

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

Wir werden das wohl auch umgehen... seufz

...

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;

Aber warten Sie, es gibt noch mehr! Mal sehen, was passiert, wenn wir Generika verwenden....

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

Wenn Sie an dieser Stelle eine Site innerhalb des Codes verwenden, die per Typoskript überprüft wird, sollten Sie wahrscheinlich einfach in den sauren Apfel beißen und sich darauf verlassen, auch wenn es wirklich weit entfernt ist.

Wenn Sie eine externe Site verwenden, überzeugen Sie die Community/Maintainer, den static -Modifikator für Interface-Mitglieder zu akzeptieren. Und in der Zwischenzeit benötigen Sie eine andere Problemumgehung. Es befindet sich nicht auf der Definitionsseite, aber ich glaube, Sie können eine modifizierte Version einer in #8328 beschriebenen Problemumgehung verwenden, um dies zu erreichen. Sehen Sie sich die Problemumgehung im Kommentar von @mhegazy vom 16. Mai 2016 mit freundlicher Genehmigung von @RyanCavanaugh an

Verzeihen Sie mir, wenn ich wichtige Punkte übersehe. Mein Verständnis des Zögerns, static für Schnittstellenmember zu unterstützen, ist in erster Linie eine Abneigung gegen die Verwendung des gleichen Schlüsselworts interface + implements , um sowohl die Form des Konstruktors als auch die Form der Instanz zu beschreiben.

Lassen Sie mich der folgenden Analogie vorausgehen, indem ich sage, dass ich liebe, was Typoskript bietet, und diejenigen, die es entwickelt haben, und die Community sehr schätze, die viel Nachdenken und Mühe in die Bewältigung der Fülle von Feature-Anfragen investiert. tun ihr Bestes, um die Funktionen zu implementieren, die gut passen.

Aber das Zögern in diesem Fall erscheint mir als Wunsch, die „konzeptionelle Reinheit“ auf Kosten der Verwendbarkeit zu bewahren. Nochmals Entschuldigung, wenn diese Analogie weit von der Basis entfernt ist:

Dies erinnerte an den Wechsel von Java zu C# . Als ich zum ersten Mal C# Code sah, der dies tat
C# var something = "something"; if(something == "something") { ... }
Die Alarmglocken begannen in meinem Kopf zu läuten, die Welt ging unter, sie mussten "something".Equals(something) verwenden usw

== ist als Referenz gleich! Sie haben ein separates .Equals(...) für den Zeichenfolgenvergleich ...
und das String-Literal muss zuerst kommen, damit Sie keine Null-Referenz erhalten, die .Equals (...) für eine Null-Variable aufruft ...
und... und... hyperventilieren

Und dann, nachdem ich C# ein paar Wochen lang verwendet hatte, wurde mir klar, wie viel einfacher es aufgrund dieses Verhaltens war, es zu verwenden. Auch wenn es bedeutete, die klare Unterscheidung zwischen den beiden aufzugeben, verbessert es die Benutzerfreundlichkeit so dramatisch, dass es sich lohnt.

So denke ich über den Modifikator static für Interface-Mitglieder. Es ermöglicht uns, die Form der Instanz weiterhin so zu beschreiben, wie wir es bereits tun, ermöglicht es uns, die Form der Konstruktorfunktion zu beschreiben, was wir nur mit mangelhaften Problemumgehungen von Fall zu Fall tun können, und ermöglicht es uns, beides relativ sauber und einfach zu tun Weg. Eine enorme Verbesserung der Benutzerfreundlichkeit, IMO.

Ich werde mich im nächsten Kommentar mit abstract static befassen....

Schnittstellen erfordern immer eine Neuspezifikation von Mitgliedstypen in implementierenden Klassen. (Die Inferenz von Mitgliedssignaturen in _explizit_ implementierenden Klassen ist ein separates, noch nicht unterstütztes Feature).

Der Grund, warum Sie Fehler erhalten, hat nichts mit diesem Angebot zu tun. Um dies zu umgehen, müssen Sie den Modifikator readonly bei der Implementierung von statischen oder anderen Klassenmitgliedern verwenden, die Literaltypen sein müssen. andernfalls wird string abgeleitet.

Auch hier ist das Schlüsselwort implements nur eine Absichtsspezifikation, es beeinflusst nicht die strukturelle Kompatibilität von Typen und führt auch keine nominelle Typisierung ein. Das soll nicht heißen, dass es nicht nützlich sein könnte, aber es ändert nicht die Typen.

Also abstract static ... es nicht wert. Zu viele Probleme.

Sie würden eine neue und komplexe Syntax benötigen, um zu deklarieren, was bereits an der Verwendungsstelle implizit überprüft wird (wenn die Verwendungsstelle vom Typoskript-Compiler überprüft wird).

Wenn die Nutzungsseite extern ist, brauchen Sie wirklich static Mitglieder einer Schnittstelle ... Entschuldigung, fertig mit dem Einstecken .... und gute Nacht!

@aluanhaddad Stimmt, aber selbst wenn die Inferenz von Mitgliedssignaturen in explizit implementierenden Klassen eine unterstützte Funktion wäre, fehlen uns saubere Möglichkeiten, die Mitgliedssignaturen für die statische Seite der Klasse zu deklarieren.

Der Punkt, den ich auszudrücken versuchte, war, dass uns Möglichkeiten fehlen, explicitly die erwartete Struktur der statischen Seite der Klasse zu deklarieren, und es gibt Fälle, in denen dies von Bedeutung ist (oder von Bedeutung sein wird, um Funktionen zu unterstützen).

In erster Linie wollte ich diesen Teil Ihres Kommentars "es spielt keine Rolle, ob die Schnittstelle(n), die die Anforderungen für die statische Seite der Klasse spezifizieren, syntaktisch referenziert wird" widerlegen.

Ich habe versucht, den Typrückschluss als Beispiel dafür zu verwenden, warum dies bei zukünftiger Unterstützung von Bedeutung sein würde .
Durch den syntaktischen Verweis auf die Schnittstelle hier let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' } müssen wir den Typ nicht explizit als 'Ja' deklarieren | 'Nein' 100 verschiedene Male.

Um dieselbe Typinferenz für die statische Seite einer Klasse (in Zukunft) zu erreichen, benötigen wir eine Möglichkeit, die Schnittstelle(n) syntaktisch zu referenzieren.

Nachdem ich Ihren letzten Kommentar gelesen habe, denke ich, dass das kein so klares Beispiel war, wie ich gehofft hatte. Ein besseres Beispiel ist vielleicht, wenn sich die Verwendungsseite für die statische Seite der Klasse in externem Code befindet, der nicht vom Typoskript-Compiler überprüft wird. In einem solchen Fall müssen wir uns derzeit auf Workarounds verlassen, die im Wesentlichen eine künstliche Nutzungsseite schaffen, eine schlechte Benutzerfreundlichkeit/Lesbarkeit bieten und in vielen Fällen nicht funktionieren.

Auf Kosten einiger Ausführlichkeit können Sie ein anständiges Maß an Typsicherheit erreichen:

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

Einverstanden, dass dies erlaubt sein sollte. Der folgende einfache Anwendungsfall würde von abstrakten statischen Methoden profitieren:

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 Ich glaube nicht, dass das noch jemand genau erwähnt hat (obwohl ich es in meinem schnellen Durchlesen hätte übersehen können), aber als Antwort auf:

Eine wichtige Frage, die wir hatten, als wir darüber nachdachten: Wer darf eine abstrakte statische Methode aufrufen? Vermutlich können Sie AbstractParentClass.getSomeClassDependentValue nicht direkt aufrufen. Aber können Sie die Methode für einen Ausdruck vom Typ AbstractParentClass aufrufen? Wenn ja, warum sollte das erlaubt sein? Wenn nicht, was nützt die Funktion?

Dies könnte durch die Implementierung eines Teils von #3841 behoben werden. Beim Lesen dieses Problems scheint der primäre Einwand zu sein, dass Arten von Konstruktorfunktionen abgeleiteter Klassen häufig nicht mit den Konstruktorfunktionen ihrer Basisklassen kompatibel sind. Das gleiche Problem scheint jedoch nicht für andere statische Methoden oder Felder zu gelten, da TypeScript bereits überprüft, ob überschriebene Statiken mit ihren Typen in der Basisklasse kompatibel sind.

Ich würde also vorschlagen, T.constructor den Typ Function & {{ statics on T }} zu geben. Das würde es abstrakten Klassen ermöglichen, die ein abstract static foo -Feld deklarieren, sicher über this.constructor.foo darauf zuzugreifen, ohne Probleme mit Konstruktor-Inkompatibilitäten zu verursachen.

Und selbst wenn das automatische Hinzufügen der Statik zu T.constructor nicht implementiert ist, könnten wir immer noch abstract static Eigenschaften in der Basisklasse verwenden, indem wir "constructor": typeof AbstractParentClass manuell deklarieren.

Ich denke, viele Leute erwarten, dass das Beispiel @patryk-zielinski93 funktionieren sollte. Stattdessen sollten wir kontraintuitive, ausführliche und kryptische Problemumgehungen verwenden. Da wir bereits Klassen und statische Member mit Syntaxzucker haben, warum können wir solchen Zucker nicht im Typsystem haben?

Hier ist mein Schmerz:

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

Vielleicht könnten wir stattdessen/parallel eine static implements -Klausel einführen? z.B

interface Foo {
    bar(): number;
}

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

Dies hat den Vorteil, dass es abwärtskompatibel mit der Deklaration separater Schnittstellen für statische und Instanzmitglieder ist, was die derzeitige Problemumgehung der Wahl zu sein scheint, wenn ich diesen Thread richtig lese. Es ist auch eine subtil andere Funktion, die ich für sich genommen als nützlich erachten könnte, aber das ist nebensächlich.

( statically implements wäre besser, aber das führt ein neues Schlüsselwort ein. Fraglich, ob das das wert ist. Es könnte jedoch kontextbezogen sein.)

Egal wie sehr ich mich bemühte, die Argumente derjenigen zu verstehen, die dem Vorschlag von OP skeptisch gegenüberstehen, ich scheiterte.

Nur eine Person (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) hat Ihnen @RyanCavanaugh geantwortet . Ich habe den Code von OP umgeschrieben, um ihn etwas sauberer zu machen und auch um Ihre Frage und meine Antworten zu veranschaulichen:

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

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

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

Wer darf eine abstrakte statische Methode aufrufen? Vermutlich können Sie Base.foo nicht direkt aufrufen

Wenn Sie ref#1 meinen, dann ja, es sollte niemals aufrufbar sein, weil es abstrakt ist und nicht einmal einen Körper hat.
Sie dürfen nur Ref#2 anrufen.

Aber können Sie die Methode für einen Ausdruck vom Typ Base aufrufen?
Wenn ja, warum sollte das erlaubt sein?

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 ist ein Fehler. Nein, Sie können "foo" dort nicht aufrufen, da baz eine __Instanz__ von Child1 oder Child2 sein soll und Instanzen keine statischen Methoden haben

ref # 4 ist (sollte) richtig sein. Sie können (sollten) dort die statische Methode foo aufrufen, da baz der Konstruktor von Child1 oder Child2 sein soll, die beide Base erweitern und daher foo() implementiert haben müssen.

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

Sie können sich diese Situation vorstellen:

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

Welchen Nutzen hat die Funktion?

Siehe Ref#4 Die Verwendung ist, wenn wir nicht wissen, welche der untergeordneten Klassen wir als Argument erhalten (es kann Child1 oder Child2 sein), aber wir möchten ihre statische Methode aufrufen und sicher sein, dass diese Methode existiert.
Ich schreibe das node.js-Framework mit dem Namen AdonisJS neu. Es wurde in reinem JS geschrieben, also wandle ich es in TS um. Ich kann die Funktionsweise des Codes nicht ändern, ich füge nur Typen hinzu. Das Fehlen dieser Funktionen macht mich sehr traurig.

ps In diesem Kommentar habe ich der Einfachheit halber nur über abstrakte Klassen geschrieben und Schnittstellen nicht erwähnt. Aber alles, was ich geschrieben habe, gilt für Schnittstellen. Sie ersetzen einfach die abstrakte Klasse durch die Schnittstelle, und alles wird korrekt sein. Es besteht die Möglichkeit, dass das TS-Team aus irgendeinem Grund (weiß nicht warum) abstract static nicht in abstract class implementieren möchte, es wäre jedoch in Ordnung, nur static zu implementieren Wort in Schnittstellen, das würde uns glücklich machen, denke ich.

pps Ich habe Klassen- und Methodennamen in Ihren Fragen bearbeitet, damit sie meinem Code entsprechen.

Ein paar Gedanken, basierend auf den Hauptargumenten anderer Skeptiker:

"Nein, das muss nicht implementiert werden, aber man kann es schon so machen *schreibt 15 mal mehr Codezeilen*".

Dafür ist syntaktischer Zucker gemacht. Lassen Sie uns ein wenig (Zucker) zu TS hinzufügen. Aber nein, wir quälen besser die Gehirne von Entwicklern, die versuchen, Dekorierer und Dutzende von Generika zu durchbrechen, anstatt den Schnittstellen ein einfaches static -Wort hinzuzufügen. Warum nicht static als einen weiteren Zucker betrachten, der unser Leben leichter macht? Genauso wie ES6 class hinzufügt (was heutzutage allgegenwärtig ist). Aber nein, lasst uns Nerds sein und alte und "richtige" Dinge tun.

"Es gibt zwei Schnittstellen für js-Klassen: für Konstruktor und Instanz".

Ok, warum geben Sie uns dann nicht eine Möglichkeit, eine Schnittstelle für den Konstruktor zu erstellen? Es ist jedoch viel einfacher und intuitiver, einfach das Wort static hinzuzufügen.

Und diesbezüglich:

Warte damit, bis wir mehr Feedback dazu hören.

Es ist mehr als ein Jahr vergangen und es wurde viel Feedback gegeben, wie lange wird diese Ausgabe verschoben? Kann bitte jemand antworten.

Dieser Beitrag mag etwas hart erscheinen ... Aber nein, es ist eine Anfrage von dem Typen, der TS liebt und gleichzeitig wirklich keinen soliden Grund finden kann, warum wir hässliche Hacks verwenden sollten, wenn wir unsere alte JS-Codebasis auf TS konvertieren. Abgesehen davon ein großes Dankeschön an das TS-Team. TS ist einfach wunderbar, es hat mein Leben verändert und ich genieße das Programmieren und meinen Job mehr denn je ... Aber dieses Problem vergiftet meine Erfahrung ...

Mögliche Problemumgehung?

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

Vorschlag: Statische Mitglieder in Schnittstellen und Typen und abstrakte Mitglieder in abstrakten Klassen, v2

Anwendungsfälle

Fantasyland-konforme Typen

````Typoskript
Schnittstelle Applikative Erweiterungen Apply {statisch von (a: A): Applikativ ; }

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

Serializable Typ mit statischer deserialize Methode

````Typoskript
Schnittstelle Serialisierbar {
static deserialize(s: string): Serialisierbar;

serialize(): string;

}
````

Verträge allgemein

````Typoskript
Schnittstellenvertrag {
static create(x: number): Vertrag;
statisch neu(x: Zahl): Vertrag;
}

const factory1 = (c: statischer Vertrag): Vertrag => c.create(Math.random());
const factory2 = (C: static Contract): Contract => new C(Math.random());
const factory3 =(c: C): new C => c.create(Math.random());

const c1 = Fabrik1 (ContractImpl); // Contract
const c2 = Fabrik2 (ContractImpl); // Contract
const c3 = Fabrik3 (ContractImpl); // ContractImpl
````

Syntax

Statische Methoden und Felder

````Typoskript
Schnittstelle Serialisierbar {
static deserialize(s: string): Serialisierbar;
}

Typ Serialisierbar = {
static deserialize(s: string): Serialisierbar;
};

abstrakte Klasse Serialisierbar {
Statisch abstrakt deserialisieren(s: string): Serialisierbar;
}
````

Statische Konstruktorsignaturen

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

Follow-on: Statische Anrufsignaturen

Diskutieren:

Wie können statische Anrufsignaturen ausgedrückt werden?
Wenn wir sie ausdrücken, indem wir einfach den Modifikator static vor der Anrufsignatur hinzufügen,
Wie werden wir sie von der Instanzmethode mit dem Namen 'static' unterscheiden?
Können wir dieselbe Problemumgehung wie für 'new' -benannte Methoden verwenden?
In einem solchen Fall wird es definitiv eine bahnbrechende Änderung sein.

Der Operator vom Typ static

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

Der Operator vom Typ new

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

Interne Steckplätze

Der [[Static]] interne Steckplatz

„Statische“ Schnittstellen eines Typs werden im internen Slot [[Static]] gespeichert:

````Typoskript
// Der Typ
Schnittstelle Serialisierbar {
static deserialize(s: string): Serialisierbar;
serialisieren: Zeichenfolge;
}

// wird intern dargestellt als
// Schnittstelle Serialisierbar {
// [[Statisch]]: {
// [[Instanz]]: Serialisierbar; // Siehe unten.
// deserialize(s: string): Serializable;
// };
// serialize(): string;
// }
````

Geben Sie standardmäßig never ein.

Der [[Instance]] interne Steckplatz

Schnittstelle vom Typ „Instanz“ wird gespeichert
im internen Slot [[Instance]] des internen Slot-Typs [[Static]] .

Geben Sie standardmäßig never ein.

Semantik

Ohne den Operator static

Wenn ein Typ als Werttyp verwendet wird,
der interne Slot [[Static]] wird verworfen:

````Typoskript
deklariere const serializable: Serializable;

Typ T = Typ von serialisierbar;
// { serialize(): string; }
````

Aber der Typ selbst behält den internen Slot [[Static]] :

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

Zuordenbarkeit

Beide Werte von statischen Typen sind den strukturell identischen Typen zuweisbar
(mit Ausnahme der [[Static]] natürlich) und umgekehrt.

Der static -Operator

| Assoziativität | Vorrang |
| :-----------: | :-------------------------------: |
| Richtig | IDK, entspricht aber dem von new 's one |

Der static gibt den Typ des internen Slots [[Static]] des Typs zurück.
Es ähnelt dem Operator vom Typ typeof ,
aber sein Argument muss eher ein Typ als ein Wert sein.

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

Der Operator vom Typ typeof verwirft auch den internen Slot [[Instance]] :

````Typoskript
deklarieren Sie const SerializableImpl: static Serializable;

Typ T = Typ von SerializableImpl;
// { deserialize(s: string): Serializable; }
````

Zuordenbarkeit

Beide Werte von instanzbewussten Typen sind den strukturell identischen Typen zuweisbar
(mit Ausnahme des [[Instance]] internen Steckplatzes natürlich) und umgekehrt.

Der new -Operator

| Assoziativität | Vorrang |
| :-----------: | :-----------------------------------: |
| Richtig | IDK, entspricht aber dem von static 's one |

Die Operatoren new geben den Typ des internen Slots [[Instance]] des Typs zurück.
Es kehrt effektiv den Operator static :

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

extends / implements Semantik

Eine Klasse, die eine Schnittstelle mit dem nicht standardmäßigen internen Slot [[Static]] implementiert
MUSS einen kompatiblen [[Static]] internen Steckplatz haben.
Die Kompatibilitätsprüfungen sollten die gleichen sein wie (oder ähnlich wie)
regelmäßige (Instanz-)Kompatibilitätsprüfungen.

````Typoskript
Klasse SerializableImpl implementiert Serializable {
statisches Deserialisieren (s: string): SerializableImpl {
// Deserialisierungslogik gehört hierher.
}

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

serialize(): string {
    //
}

}
````

MACHEN

  • [ ] Entscheiden Sie, welche Syntax für statische Rufsignaturen verwendet werden soll.
    Möglicherweise ohne Breaking Changes.
  • [ ] Gibt es Sonderfälle mit bedingten Typen und dem Operator infer ?
  • [ ] Änderungen an der Semantik der nicht abstrakten Klassenmitglieder. _Kann kaputt gehen._

Ich habe ein paar Stunden damit verbracht, dieses Problem und andere Probleme durchzulesen, um eine Problemumgehung für mein Problem zu finden, das durch den statischen Modifikator oder die Schnittstellen für Klassen gelöst werden würde. Ich kann keinen einzigen Workaround finden, der mein Problem löst.

Das Problem, das ich habe, ist, dass ich eine Änderung an einer Code-generierten Klasse vornehmen möchte. Was ich zum Beispiel machen möchte ist:

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;

Ich kann keine einzige Problemumgehung finden, die es mir ermöglicht, das Äquivalent dazu mit der aktuellen Version von TS zu tun. Übersehe ich derzeit eine Möglichkeit, dieses Problem zu lösen?

Dies erscheint relevant, um hier als Anwendungsfall zu posten, für den es anscheinend keine Problemumgehung und sicherlich keine einfache Problemumgehung gibt.

Wenn Sie die Instanz und die statische Seite einer Klasse mit Schnittstellen vergleichen möchten, können Sie dies folgendermaßen tun:

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

Allzu viele von uns verschwenden viel zu viele Stunden ihrer Zeit und anderer damit. Warum ist es kein Ding?!¿i!
Warum sind die Antworten alle große Problemumgehungen für etwas, das lesbar, verdaulich und etwas sein sollte, das ein Einzeiler ist, der einfach ist. Ich möchte auf keinen Fall, dass jemand mit einer hackigen Problemumgehung herausfinden muss, was mein Code zu tun versucht. Tut mir leid, dieses Thema mit etwas zu überladen, das nicht sehr wertvoll ist, aber es ist derzeit ein großer Schmerz und Zeitverschwendung, also finde ich es wertvoll für mich, die anderen jetzt und diejenigen, die später nach einer Antwort suchen.

Die Sache mit jeder Sprache, sowohl natürlich als auch künstlich, dass sie sich natürlich entwickeln sollte, um effizient und ansprechend zu sein. Schließlich entschieden sich die Leute für Reduktionen in der Sprache ("okay"=>"ok","going to" =>"gonna"), erfanden neue lächerliche Wörter wie "selfie" und "google", definierten die Schreibweise mit l33tspeak und neu Sachen, und sogar einige Wörter verboten, und egal ob Sie sie verwenden wollen oder nicht, jeder versteht immer noch, was sie bedeuten, und einige von uns verwenden sie, um bestimmte Aufgaben zu erledigen. Und für keines davon kann es einen triftigen Grund geben, aber die Effizienz bestimmter Personen bei bestimmten Aufgaben ist alles eine Frage der Anzahl von Personen, die sie tatsächlich nutzen. Der Umfang dieses Gesprächs zeigt deutlich, dass eine ganze Menge Leute dieses static abstract für ihre gottverdammten Überlegungen gebrauchen könnten. Ich bin aus dem gleichen Grund hierher gekommen, weil ich Serializable implementieren wollte, also habe ich alle intuitiven (für mich) Methoden ausprobiert, aber keine davon funktioniert. Vertrauen Sie mir, das Letzte, wonach ich suchen würde, ist die Erklärung, warum ich diese Funktion nicht brauche und mich für etwas anderes entscheiden sollte. Anderthalb Jahre, Jesus Christus! Ich wette, es gibt schon irgendwo eine PR mit Tests und so. Bitte machen Sie dies möglich, und wenn es eine bestimmte Art der Verwendung gibt, von der abgeraten wird, haben wir einen Tslint dafür.

Es ist möglich, über this.constructor.staticMember von der Basisklasse aus auf statische Member von untergeordneten Klassen zuzugreifen, daher sind abstrakte statische Member für mich sinnvoll.

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

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

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

Klasse A sollte angeben können, dass sie ein statisches x -Member benötigt, da sie es in der Methode f verwendet.

Irgendwelche Neuigkeiten ?
Die Features werden wirklich gebraucht 😄
1) static Funktionen in Schnittstellen
2) abstract static Funktionen in abstrakten Klassen

Ich hasse zwar die Idee, statische Schnittstellen zu haben, aber für alle praktischen Zwecke sollte das Folgende heute ausreichen:

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

das kann verwendet werden als:

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

AKTUALISIEREN:

Weitere Details zur Idee und ein Live- Beispiel :

// 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, das ist vielleicht nicht die Schuld von Typescript, aber ich konnte diese funktionierenden Angular-Komponenten-Dekoratoren und ihren AoT-Compiler nicht bekommen.

@aleksey-bykov das ist clever, funktioniert aber immer noch nicht für abstrakte Statik. Wenn Sie Unterklassen von MyClass haben, werden diese nicht durch die Typprüfung erzwungen. Es ist auch schlimmer, wenn Sie Generika im Spiel haben.

// no errors
class Thing extends MyClass {

}

Ich hoffe wirklich, dass das TypeScript-Team seine Haltung dazu überdenkt, denn das Erstellen von Endbenutzerbibliotheken, die statische Attribute erfordern, hat keine vernünftige Implementierung. Wir sollten in der Lage sein, einen Vertrag zu haben, der erfordert, dass Schnittstellenimplementierer/Extender abstrakter Klassen über Statik verfügen.

@bbugh Ich stelle die Existenz des hier diskutierten Problems in Frage. Warum sollten Sie all diese Probleme mit statischen abstrakten geerbten Methoden benötigen, wenn dies auch über Instanzen regulärer Klassen möglich ist?

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

vs

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

@aleksey-bykov Es gibt viele Gründe. Zum Beispiel ( von @patryk-zielinski93):

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

Ich möchte die Implementierung der statischen Deserialisierungsmethode in den Unterklassen von Serializable erzwingen.
Gibt es eine Problemumgehung, um ein solches Verhalten zu implementieren?

EDIT: Ich weiß, dass Sie auch deserialize als Konstruktor verwenden könnten, aber Klassen können nur 1 Konstruktor haben, was Factory-Methoden erforderlich macht. Die Leute wollen eine Möglichkeit, Factory-Methoden in Schnittstellen zu fordern, was absolut sinnvoll ist.

Bringen Sie die Deserialisierungslogik einfach in eine separate Klasse, da es keinen Vorteil bringt, eine statische Deserialisierungsmethode an die zu deserialisierende Klasse angehängt zu haben

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

was ist das Problem?

@aleksey-bykov separate Klasse sieht nicht so hübsch aus

@aleksey-bykov, das Problem ist, dass es mehr als eine Klasse gibt, die eine Serialisierung erfordert. Ihr Ansatz zwingt also dazu, ein Wörterbuch mit Serialisierbaren zu erstellen, bei dem es sich um ein Überklassen-Antimuster handelt, und jede Änderung an einer von ihnen würde eine Bearbeitung in dieser Überklasse erfordern. was die Codeunterstützung zu einem Ärgernis macht. Eine serialisierbare Schnittstelle könnte zwar eine Implementierung für jeden beliebigen Objekttyp erzwingen und auch polymorphe Vererbung unterstützen, was die Hauptgründe dafür sind, dass die Leute es wollen. Bitte spekulieren Sie nicht, ob es einen Nutzen gibt oder nicht, jeder sollte in der Lage sein, Optionen zu haben und zu wählen, was für sein eigenes Projekt von Vorteil ist.

@octaharon eine Klasse zu haben, die ausschließlich der Deserialisierung gewidmet ist, ist genau das Gegenteil von dem, was Sie gesagt haben. Sie hat eine einzige Verantwortung, da Sie sie nur dann ändern, wenn Sie sich um die Deserialisierung kümmern

Im Gegensatz dazu gibt das Hinzufügen von Deserialisierung zur Klasse selbst, wie Sie vorgeschlagen haben, ihr eine zusätzliche Verantwortung und einen zusätzlichen Grund für die Änderung

Schließlich habe ich kein Problem mit statischen Methoden, aber ich bin wirklich neugierig auf ein Beispiel für einen praktischen Anwendungsfall für abstrakte statische Methoden und statische Schnittstellen

@octaharon eine Klasse zu haben, die ausschließlich der Deserialisierung gewidmet ist, ist genau das Gegenteil von dem, was Sie gesagt haben. Sie hat eine einzige Verantwortung, da Sie sie nur dann ändern, wenn Sie sich um die Deserialisierung kümmern

außer dass Sie den Code an zwei statt an einer Stelle ändern müssen, da Ihre (De-)Serialisierung von Ihrer Typstruktur abhängt. Das zählt nicht zur "Einzelverantwortung"

Im Gegensatz dazu gibt das Hinzufügen von Deserialisierung zur Klasse selbst, wie Sie vorgeschlagen haben, ihr eine zusätzliche Verantwortung und einen zusätzlichen Grund für die Änderung

Ich verstehe nicht, was Sie sagen. Eine Klasse, die für ihre eigene (De-)Serialisierung verantwortlich ist, ist genau das, was ich will, und bitte sagen Sie mir nicht, ob es richtig oder falsch ist.

Einzelverantwortung sagt nichts darüber aus, wie viele Dateien beteiligt sind, es sagt nur, dass es einen einzigen Grund geben muss

Was ich sagen will, ist, dass Sie, wenn Sie eine neue Klasse hinzufügen, sie für etwas anderes als nur für die Deserialisierung meinen, also hat sie bereits einen Existenzgrund und eine ihr zugewiesene Verantwortung; dann fügen Sie eine weitere Verantwortung hinzu, sich selbst deserialisieren zu können, dies gibt uns 2 Verantwortlichkeiten, dies verstößt gegen SOLID, wenn Sie immer mehr Dinge hinzufügen, wie Rendern, Drucken, Kopieren, Übertragen, Verschlüsseln usw., erhalten Sie eine Gottklasse

und entschuldigen Sie, dass ich Ihnen Banalitäten erzähle, aber Fragen (und Antworten) zu Vorteilen und praktischen Anwendungsfällen sind es, die diesen Funktionsvorschlag zur Implementierung antreiben

SOLID wurde nicht von Gott dem Allmächtigen auf einem Tablet gesendet, und selbst wenn es so wäre, gibt es einen gewissen Freiheitsgrad bei seiner Interpretation. Sie können so idealistisch sein, wie Sie möchten, aber tun Sie mir einen Gefallen: Verbreiten Sie Ihre Überzeugungen nicht in der Gemeinschaft. Jeder hat alle Rechte, jedes Werkzeug so zu benutzen, wie er will, und alle möglichen Regeln zu brechen, die er kennt (man kann einem Messer nicht die Schuld für einen Mord geben). Was die Qualität eines Tools ausmacht, ist ein Gleichgewicht zwischen einer Nachfrage nach bestimmten Funktionen und einem Angebot dafür. Und diese Branche zeigt das Volumen der Nachfrage. Wenn Sie diese Funktion nicht benötigen, verwenden Sie sie nicht. Ich mache. Und ein paar Leute hier brauchen es auch, und _wir_ haben einen praktischen Anwendungsfall, während Sie sagen, dass wir es eigentlich ignorieren sollten. Es geht nur darum, was für die Betreuer wichtiger ist – die heiligen Prinzipien (wie auch immer sie es verstehen) oder die Gemeinschaft.

Mann, jeder gute Vorschlag enthält Anwendungsfälle, dieser nicht

image

Also habe ich nur etwas Neugier zum Ausdruck gebracht und eine Frage gestellt, da der Vorschlag es nicht sagt, warum Sie Leute es brauchen könnten

es lief auf typisch hinaus: nur weil du es nicht wagst

persönlich ist mir solide oder oop egal, ich habe es einfach vor langer Zeit überwuchert, Sie haben es angesprochen, indem Sie das Argument "Überklassen-Antimuster" geworfen und dann auf "einen Grad an Freiheit für seine Interpretation" zurückgegriffen haben.

Der einzige praktische Grund, der in dieser ganzen Diskussion erwähnt wird, ist dieser: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119

Ein sehr häufiger Anwendungsfall, bei dem dies hilfreich ist, sind React-Komponenten, bei denen es sich um Klassen mit statischen Eigenschaften wie displayName, propTypes und defaultProps handelt.

und ein paar Beiträge wie https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -345496014

aber es ist von (new (...) => MyClass) & MyStaticInterface gedeckt

das ist der genaue Anwendungsfall und ein Grund, warum ich hier bin. Sehen Sie die Anzahl der Stimmen? Warum denkst du, es liegt an dir persönlich zu entscheiden, was practical ist und was nicht? Practical ist das, was in practice gesteckt werden kann, und (zum Zeitpunkt des Schreibens) würden 83 Personen diese Funktion verdammt praktisch finden. Bitte respektieren Sie andere und lesen Sie den gesamten Thread, bevor Sie anfangen, die Phrasen aus dem Kontext zu ziehen und verschiedene Schlagworte zur Schau zu stellen. Was auch immer du überwunden hast, das ist definitiv nicht dein Ego.

Es ist gesunder Menschenverstand, dass praktische Dinge diejenigen sind, die Probleme lösen, nicht praktische Dinge diejenigen, die Ihren Sinn für Schönheit anregen löst gegebenes (new (...) => MyClass) & MyStaticInterface für https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119 und https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -289084844 nur Ursache

bitte nicht antworten

Aus dem gleichen Grund verwende ich manchmal type -Deklarationen, um große Anmerkungen zu reduzieren. Ich denke, ein Schlüsselwort wie abstract static wäre viel besser lesbar als ein relativ schwieriger zu verdauliches Konstrukt, wie es als vorhanden erwähnt wurde Beispiel.

Außerdem haben wir uns noch nicht mit abstrakten Klassen befasst?

Die Lösung, keine abstrakten Klassen zu verwenden, ist meiner Meinung nach nicht die Lösung. Das ist eine Umgehung! Ein Workaround um was herum?

Ich denke, diese Feature-Anfrage existiert, weil viele Leute, einschließlich des Anforderers, festgestellt haben, dass ein erwartetes Feature wie abstract static oder static in Schnittstellen nicht vorhanden war.

Muss das Schlüsselwort static basierend auf der angebotenen Lösung überhaupt existieren, wenn es eine Problemumgehung gibt, um seine Verwendung zu vermeiden? Ich denke, das wäre genauso lächerlich, das vorzuschlagen.

Das Problem hier ist, dass static viel mehr Sinn macht.
Können wir basierend auf dem erzeugten Interesse eine weniger abweisende Diskussion führen?

Gibt es Neuigkeiten zu diesem Vorschlag? Irgendwelche bedenkenswerten Argumente, die zeigen, warum wir static abstract und dergleichen nicht haben sollten?
Können wir weitere Vorschläge haben, die zeigen, warum es nützlich wäre?

Vielleicht müssen wir die Dinge einholen und zusammenfassen, was besprochen wurde, damit wir eine Lösung finden können.

Soweit ich weiß gibt es zwei Vorschläge:

  1. Schnittstellen und Typen können statische Eigenschaften und Methoden definieren
interface ISerializable<T> { 
   static fromJson(json: string): T;
}
  1. abstrakte Klassen können abstrakte statische Methoden definieren
abstract class MyClass<T> implements ISerializable<T> {
   abstract static fromJson(json: string): T;
}

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

Was den ersten Vorschlag betrifft, gibt es technisch gesehen eine Umgehung! Was nicht toll ist, aber das ist immerhin etwas. Aber es ist ein Workaround.

Sie können Ihre Schnittstellen in zwei Teile aufteilen und Ihre Klasse wie neu schreiben

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

interface InstanceInterface {
  toJson(): string;
}

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

Meiner Meinung nach ist dies eine Menge zusätzlicher Arbeit und etwas weniger lesbar und hat den Nachteil, dass Sie Ihre Klassen auf eine komische Weise umschreiben, die einfach seltsam ist und von der von uns verwendeten Syntax abweicht.

Aber was ist dann mit Vorschlag 2? Dagegen kann man nichts machen, oder? Ich denke, das verdient es, auch angesprochen zu werden!

Was ist der praktische Nutzen für einen dieser Typen – wie würde einer von diesen verwendet werden?

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

Es ist bereits möglich zu sagen, dass ein Wert ein Objekt wie { fromJSON(serializedValue: string): JsonSerializable; } sein muss, also ist dies nur gewollt, um ein Muster zu erzwingen? Ich sehe keinen Vorteil darin aus Sicht der Typprüfung. Als Randnotiz: In diesem Fall würde es ein Muster erzwingen, mit dem man nur schwer arbeiten kann – es wäre aus vielen Gründen besser, den Serialisierungsprozess in separate Serializer-Klassen oder -Funktionen zu verschieben, auf die ich hier nicht eingehen werde.

Außerdem, warum wird so etwas gemacht?

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

Wie wäre es, stattdessen entweder die Vorlagenmethode oder das Strategiemuster zu verwenden? Das würde funktionieren und flexibler sein, oder?

Im Moment bin ich gegen dieses Feature, weil es meiner Meinung nach unnötige Komplexität hinzufügt, um Klassendesigns zu beschreiben, mit denen schwer zu arbeiten ist. Vielleicht gibt es aber einen Vorteil, den ich übersehe?

Es gibt einen gültigen Anwendungsfall für statische React-Methoden, das war's

@aleksey-bykov Ah, ok. In diesen Fällen könnte es besser sein, wenn das Hinzufügen eines Typs zur Eigenschaft constructor in diesem seltenen Szenario eine Typüberprüfung verursachen würde. Zum Beispiel:

interface Component {
    constructor: ComponentConstructor;
}

interface ComponentConstructor {
    displayName?: string;
}

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

Das scheint mir viel lohnender. Es fügt der Sprache keine zusätzliche Komplexität hinzu und fügt dem Typprüfer nur mehr Arbeit hinzu, wenn er prüft, ob eine Klasse eine Schnittstelle richtig implementiert.


Übrigens dachte ich, ein gültiger Anwendungsfall wäre, wenn man von einer Instanz zum Konstruktor und schließlich zu einer statischen Methode oder Eigenschaft mit Typüberprüfung wandert, aber das ist bereits möglich, indem man die Eigenschaft constructor für einen Typ eingibt und wird in #3841 für Klasseninstanzen gelöst.

Ich hatte eine ähnliche Idee: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

Ist das im Upthread beschriebene Serialisierungs-/Deserialisierungsmuster nicht "gültig"?

@dsherret "sieht den Vorteil aus Sicht der Typprüfung nicht". Der springende Punkt bei der statischen Analyse ist in erster Linie, Fehler so früh wie möglich zu erkennen. Ich tippe Dinge so, dass, wenn eine Anrufsignatur geändert werden muss, jeder, der sie aufruft – oder, was entscheidend ist, jeder, der für die Implementierung von Methoden verantwortlich ist, die die Signatur verwenden – auf die neue Signatur aktualisiert.

Angenommen, ich habe eine Bibliothek, die eine Reihe von Geschwisterklassen mit einer statischen foo(x: number, y: boolean, z: string) -Methode bereitstellt, und die Erwartung ist, dass Benutzer eine Factory-Klasse schreiben, die mehrere dieser Klassen verwendet und die Methode aufruft, um Instanzen zu erstellen. (Vielleicht ist es deserialize oder clone oder unpack oder loadFromServer , spielt keine Rolle.) Der Benutzer erstellt auch Unterklassen desselben (möglicherweise abstrakten) Elternteils Klasse aus der Bibliothek.

Jetzt muss ich diesen Vertrag so ändern, dass der letzte Parameter ein Optionsobjekt ist. Das Übergeben eines Zeichenfolgenwerts für das dritte Argument ist ein nicht behebbarer Fehler, der zur Kompilierzeit gekennzeichnet werden sollte. Jede Fabrikklasse sollte aktualisiert werden, um stattdessen ein Objekt zu übergeben, wenn foo aufgerufen wird, und Unterklassen, die foo implementieren, sollten ihre Aufrufsignatur ändern.

Ich möchte garantieren, dass Benutzer, die auf die neue Bibliotheksversion aktualisieren, die Breaking Changes zur Kompilierzeit erfassen. Die Bibliothek muss eine der oben genannten Problemumgehungen für die statische Schnittstelle (wie type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface; ) exportieren und hoffen, dass der Verbraucher sie an den richtigen Stellen angewendet hat. Wenn sie vergessen haben, eine ihrer Implementierungen zu dekorieren, oder wenn sie keinen aus der Bibliothek exportierten Typ verwendet haben, um die Aufrufsignatur von foo in ihrer Factory-Klasse zu beschreiben, kann der Compiler nichts feststellen, was sich geändert hat, und sie erhalten Laufzeitfehler . Vergleichen Sie das mit einer vernünftigen Implementierung von abstract static -Methoden in der übergeordneten Klasse – es sind keine speziellen Anmerkungen erforderlich, keine Belastung des verbrauchenden Codes, es funktioniert einfach sofort.

@thw0rted erfordern die meisten Bibliotheken nicht eine Art Registrierung dieser Klassen und kann an diesem Punkt eine Typprüfung erfolgen? Zum Beispiel:

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

Oder werden Instanzen dieser Klassen mit der Bibliothek verwendet und die Bibliothek geht von der Instanz zum Konstruktor, um die statischen Methoden zu erhalten? In diesem Fall ist es möglich, das Design der Bibliothek so zu ändern, dass keine statischen Methoden erforderlich sind.

Als Nebenbemerkung, als Implementierer einer Schnittstelle, würde ich mich sehr ärgern, wenn ich gezwungen wäre, statische Methoden zu schreiben, da es sehr schwierig ist, Abhängigkeiten in eine statische Methode zu injizieren, da kein Konstruktor vorhanden ist (es sind keine Ctor-Abhängigkeiten möglich). Es macht es auch schwierig, die Implementierung der Deserialisierung je nach Situation gegen etwas anderes auszutauschen. Angenommen, ich habe aus verschiedenen Quellen mit unterschiedlichen Serialisierungsmechanismen deserialisiert ... jetzt habe ich eine statische Deserialisierungsmethode, die per Design nur auf eine Weise für eine Implementierung einer Klasse implementiert werden kann. Um das zu umgehen, müsste ich eine andere globale statische Eigenschaft oder Methode in der Klasse haben, um der statischen Deserialisierungsmethode mitzuteilen, was sie verwenden soll. Ich würde eine separate Serializer -Schnittstelle bevorzugen, die ein einfaches Austauschen der zu verwendenden Elemente ermöglicht und die (De-)Serialisierung nicht an die Klasse koppelt, die (de-)serialisiert wird.

Ich verstehe, was Sie meinen - um ein Fabrikmuster zu verwenden, müssen Sie schließlich die implementierende Klasse an die Fabrik übergeben, und zu diesem Zeitpunkt erfolgt die statische Überprüfung. Ich denke immer noch, dass es Zeiten geben kann, in denen Sie eine Factory-konforme Klasse bereitstellen möchten, sie aber im Moment nicht wirklich verwenden. In diesem Fall würde eine abstract static -Einschränkung Probleme früher erkennen und die Bedeutung klarer machen.

Ich habe ein weiteres Beispiel, von dem ich denke, dass es Ihrer "Nebenbemerkung" nicht widerspricht. Ich habe eine Reihe von Geschwisterklassen in einem Frontend-Projekt, in denen ich dem Benutzer die Wahl lasse, welchen "Anbieter" er für eine bestimmte Funktion verwenden soll. Natürlich könnte ich aus name => Provider ein Wörterbuch machen und die Schlüssel verwenden, um zu bestimmen, was in der Auswahlliste angezeigt werden soll, aber die Art und Weise, wie ich es jetzt implementiert habe, besteht darin, dass ein statisches name -Feld erforderlich ist auf jeder Provider-Implementierung.

Irgendwann habe ich das so geändert, dass sowohl shortName als auch longName erforderlich sind (für die Anzeige in verschiedenen Kontexten mit unterschiedlich viel verfügbarem Bildschirmplatz). Es wäre viel einfacher gewesen, abstract static name: string; in abstract static shortName: string; usw. zu ändern, anstatt die Auswahllistenkomponente in providerList: Type<Provider> & { shortName: string } & { longName: string } zu ändern. Es vermittelt die Absicht an der richtigen Stelle (d. h. in der abstrakten Elternkomponente, nicht in der konsumierenden Komponente) und ist leicht zu lesen und leicht zu ändern. Ich denke, wir können sagen, dass es eine Problemumgehung gibt, aber ich glaube immer noch, dass sie objektiv schlechter ist als die vorgeschlagenen Änderungen.

Ich bin kürzlich auf dieses Problem gestoßen, als ich eine statische Methode in einer Schnittstelle verwenden musste. Ich bitte um Entschuldigung, falls dies bereits angesprochen wurde, da ich keine Zeit habe, diese Menge an Kommentaren durchzulesen.

Mein aktuelles Problem: Ich habe eine private .js-Bibliothek, die ich in meinem TypeScript-Projekt verwenden möchte. Also habe ich angefangen, eine .d.ts-Datei für diese Bibliothek zu schreiben, aber da die Bibliothek statische Methoden verwendet, konnte ich das nicht wirklich beenden. Welche Vorgehensweise wird in diesem Fall vorgeschlagen?

Danke für Antworten.

@greeny hier ist eine funktionierende Lösung: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

speziell dieser Teil davon:

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

Ein weiterer Anwendungsfall: Generierter Code / partielle Klassen-Shims

  • Ich verwende die @rsuter/nswag-Bibliothek, die Swagger-Spezifikationen generiert.
  • Sie können eine Erweiterungsdatei schreiben, die in die generierte Datei eingefügt wird.
  • Die Erweiterungsdatei wird nie ausgeführt, muss aber selbst kompiliert werden!
  • Manchmal muss ich in dieser Datei auf etwas verweisen, das noch nicht existiert (weil es generiert wurde).
  • Daher möchte ich dafür wie folgt ein Shim / Interface deklarieren
  • Hinweis: Sie können angeben, dass bestimmte import -Anweisungen bei der endgültigen Zusammenführung ignoriert werden, sodass das 'shim'-Include im endgültig generierten Code ignoriert wird.
interface SwaggerException
{
    static isSwaggerException(obj: any): obj is SwaggerException;
}

Aber ich kann das nicht und muss tatsächlich eine Klasse schreiben - was in Ordnung ist, sich aber trotzdem falsch anfühlt. Ich möchte nur - wie viele andere gesagt haben - sagen: "Das ist der Vertrag".

Ich wollte dies hier nur hinzufügen, da ich keine anderen Erwähnungen der Codegenerierung sehe - aber viele "Warum würden Sie das brauchen" -Kommentare, die einfach irritierend werden. Ich würde erwarten, dass ein anständiger Prozentsatz der Leute, die diese „statische“ Funktion in einer Schnittstelle „benötigen“, dies nur tun, damit sie auf externe Bibliothekselemente verweisen können.

Außerdem würde ich gerne sauberere d.ts -Dateien sehen - es muss einige Überschneidungen mit dieser Funktion und diesen Dateien geben. Es ist schwer, so etwas wie JQueryStatic zu verstehen, weil es so aussieht, als wäre es nur ein Hack. Die Realität ist auch, dass d.ts Dateien oft veraltet sind und nicht gepflegt werden und Sie Shims selbst deklarieren müssen.

(Entschuldigung für die Erwähnung von jQuery)

Für den Serialisierungsfall habe ich so etwas gemacht.

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

Aber es wäre noch viel einfacher, so etwas zu verwenden:

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

Ich habe diesen Thread viel gelesen und verstehe immer noch nicht, warum es gut und richtig ist, zu diesem Muster nein zu sagen. Wenn Sie diese Meinung teilen, sagen Sie auch, Java und andere populäre Sprachen machen es falsch. Oder liege ich falsch?

Diese Ausgabe ist zwei Jahre geöffnet und hat 79 Kommentare. Es ist als Awaiting More Feedback gekennzeichnet. Können Sie sagen, welches Feedback Sie sonst noch brauchen, um eine Entscheidung zu treffen?

Es ist wirklich ärgerlich, dass ich statische Methoden weder in der Schnittstelle noch in der abstrakten Klasse (nur Deklaration) beschreiben kann. Es ist so hässlich, Problemumgehungen zu schreiben, weil diese Funktion noch implementiert werden muss =(

Zum Beispiel verwendet next.js eine statische getInitialProps -Funktion zum Abrufen von Seiteneigenschaften, bevor die Seite erstellt wird. Falls es wirft, wird nicht die Seite aufgebaut, sondern die Fehlerseite.

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

Aber leider können Hinweis ts, die diese Methode implementieren, ihr jede Typsignatur geben, selbst wenn sie zur Laufzeit Fehler verursacht, weil sie nicht typgeprüft werden kann.

Ich denke, dieses Problem besteht hier so lange, weil JavaScript selbst nicht gut für statische Dinge ist 🤔

Statische Vererbung sollte an erster Stelle nie existieren. 🤔🤔

Wer möchte eine statische Methode aufrufen oder ein statisches Feld lesen? 🤔🤔🤔

  • untergeordnete Klasse: Sie darf nicht statisch sein
  • Elternklasse: Konstruktorargument verwenden
  • Sonstiges: IsomeClassConstructor-Schnittstelle verwenden

Gibt es einen anderen Anwendungsfall?🤔🤔🤔🤔

Gibt es dazu überhaupt ein Update?

Wenn es hilfreich ist, habe ich in Fällen, in denen ich die statische Schnittstelle für eine Klasse eingeben muss, einen Dekorator verwendet, um die statischen Member für die Klasse zu erzwingen

Der Dekorateur ist definiert als:

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

Wenn ich eine statische Konstruktor-Member-Schnittstelle definiert habe als

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

und in der Klasse aufgerufen, die die Mitglieder in T statisch deklarieren sollte als:

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

Das häufigste Beispiel ist gut:

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

Aber wie in #13462 hier gesagt:

Schnittstellen sollten die Funktionalität definieren, die ein Objekt bereitstellt. Diese Funktionalität sollte überschreibbar und austauschbar sein (deshalb sind Schnittstellenmethoden virtuell). Statik ist ein paralleles Konzept zu dynamischem Verhalten/virtuellen Methoden.

Ich stimme dem Punkt zu, dass Schnittstellen in TypeScript nur eine Objektinstanz selbst beschreiben und wie sie verwendet wird. Das Problem besteht darin, dass eine Objektinstanz keine Klassendefinition ist und ein static -Symbol möglicherweise nur in einer Klassendefinition existiert.

Also kann ich folgendes vorschlagen, mit all seinen Mängeln:


Eine Schnittstelle kann entweder ein Objekt oder eine Klasse beschreiben. Nehmen wir an, dass eine Klassenschnittstelle mit den Schlüsselwörtern class_interface notiert wird.

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

Klassen (und Klassenschnittstellen) könnten die Schlüsselwörter statically implements verwenden, um ihre statischen Symbole unter Verwendung einer Objektschnittstelle zu deklarieren ( Klassenschnittstellen konnten nicht statically implementiert werden).

Klassen (und Klassenschnittstellen) würden weiterhin das Schlüsselwort implements mit einer Objektschnittstelle oder einer Klassenschnittstelle verwenden.

Eine Klassenschnittstelle könnte dann die Mischung aus einer statisch implementierten Objektschnittstelle und einer instanzimplementierten Schnittstelle sein. Somit könnten wir Folgendes erhalten:

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

class_interface ISerDes implements ISerializable statically implements IDeserializable {}

Auf diese Weise könnten Schnittstellen ihre Bedeutung behalten, und class_interface wäre eine neue Art von Abstraktionssymbol für Klassendefinitionen.

Ein kleiner unkritischer Anwendungsfall mehr:
In Angular für die AOT-Kompilierung können Sie keine Funktionen in Dekoratoren aufrufen (wie im Modul-Dekorator @NgModule ).
eckiges Problem

Für das Service Worker-Modul benötigen Sie Folgendes:
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
Unsere Umgebung verwendet Unterklassen, erweitert die abstrakte Klasse mit Standardwerten und zu implementierenden abstrakten Eigenschaften. Ähnliches Implementierungsbeispiel
AOT funktioniert also nicht, weil das Konstruieren einer Klasse eine Funktion ist und einen Fehler auslöst wie: Function calls are not supported in decorators but ..

Damit es funktioniert und die Autovervollständigung/Compiler-Unterstützung erhalten bleibt, ist es möglich, dieselben Eigenschaften auf statischer Ebene zu definieren. Aber um Eigenschaften zu implementieren, benötigen wir eine Schnittstelle mit statischen Membern oder abstrakten statischen Membern in der abstrakten Klasse. Beides noch nicht möglich.

mit Schnittstelle könnte so funktionieren:

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

mit abstrakter Statik könnte so funktionieren:

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

Es gibt Problemumgehungen , aber alle sind schwach :/

@DanielRosenwasser @RyanCavanaugh entschuldigt sich für die Erwähnungen, aber es scheint, dass dieser Funktionsvorschlag – der viel Unterstützung von der Community erhält und meiner Meinung nach ziemlich einfach zu implementieren wäre – tief in der Kategorie „Probleme“ vergraben wurde. Hat einer von Ihnen irgendwelche Kommentare zu dieser Funktion und wäre eine PR willkommen?

Ist diese Ausgabe nicht ein Duplikat von Nr. 1263? 😛

26398 (Typprüfung statischer Member basierend auf der Konstruktoreigenschaft des Typs implements) sieht nach einer besseren Lösung aus ... wenn so etwas implementiert werden sollte, würde ich hoffen, dass es das ist. Dies erfordert keine zusätzliche Syntax/Analyse und ist nur eine Typprüfungsänderung für ein einzelnes Szenario. Es wirft auch nicht so viele Fragen auf.

Ich habe das Gefühl, dass statische Methoden in Schnittstellen nicht so intuitiv sind wie statische abstrakte Methoden in abstrakten Klassen.

Ich denke, es ist ein wenig skizzenhaft, Schnittstellen statische Methoden hinzuzufügen, da eine Schnittstelle ein Objekt und keine Klasse definieren sollte. Andererseits sollte eine abstrakte Klasse unbedingt statische abstrakte Methoden haben dürfen, da abstrakte Klassen zur Definition von Unterklassen verwendet werden. Was die Implementierung angeht, müsste es einfach nur beim Erweitern der abstrakten Klasse (z. B. class extends MyAbstractClass ) typgeprüft werden, nicht bei der Verwendung als Typ (z. B. let myInstance: MyAbstractClass ).

Beispiel:

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

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

jetzt aus der Not benutze ich das

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

das ist unpraktisch!

Ich hatte ein Problem, bei dem ich dem "Objekt" Eigenschaften hinzufüge, hier ist ein Sandbox-Beispiel

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

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

Alle Objektinstanzen haben jetzt die Eigenschaft getInstanceId , während nur die Object dies tun sollten. Mit einer statischen Eigenschaft wäre das Problem gelöst.

Sie möchten ObjectConstructor erweitern, nicht Object. Sie deklarieren eine Instanzmethode, wenn Sie eigentlich eine Methode an den Konstruktor selbst anhängen möchten. Ich denke, dies ist bereits durch das Zusammenführen von Deklarationen möglich:

````ts
global deklarieren {
Schnittstelle ObjectConstructor {
Hallo(): Zeichenkette;
}
}

Objekt.hallo();
````

@thw0rted Ausgezeichnet! danke, ich war mir ObjectConstructor nicht bewusst

Der wichtigere Punkt ist, dass Sie eher den Konstruktortyp als den Instanztyp erweitern. Ich habe gerade die Deklaration Object in lib.es5.d.ts nachgeschlagen und festgestellt, dass sie vom Typ ObjectConstructor ist.

Es fällt mir schwer zu glauben, dass es dieses Problem noch gibt. Dies ist eine legitim nützliche Funktion mit mehreren tatsächlichen Anwendungsfällen.

Der ganze Sinn von TypeScript besteht darin, die Typsicherheit in unserer Codebasis gewährleisten zu können, also warum ist diese Funktion nach zwei Jahren Feedback immer noch "ausstehend für Feedback"?

Ich bin vielleicht weit davon entfernt, aber wäre etwas wie die Metaklassen von Python in der Lage, dieses Problem auf native, sanktionierte Weise (dh nicht durch einen Hack oder eine Problemumgehung) zu lösen, ohne das Paradigma von TypeScript zu verletzen (dh separate TypeScript-Typen für die Instanz und die Klasse)?

Etwas wie das:

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

Ehrlich gesagt scheint der kniffligste Teil dieses Ansatzes so zu sein, als würde er ein sinnvolles, TypeScript-artiges Schlüsselwort für metaclass ... staticimplements , classimplements , withstatic finden implementsstatic ... nicht sicher.

Dies ist ein bisschen wie der Vorschlag von @ GerkinDev , aber ohne die separate Art von Schnittstelle. Hier gibt es ein einziges Konzept von Schnittstellen, und sie können verwendet werden, um die Form einer Instanz oder einer Klasse zu beschreiben. Schlüsselwörter in der implementierenden Klassendefinition würden dem Compiler mitteilen, gegen welche Seite jede Schnittstelle geprüft werden sollte.

Lassen Sie uns die Diskussion bei #34516 und #33892 aufnehmen, je nachdem, welche Funktion Sie anstreben

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen