Typescript: Suggestion : Ajoutez des méthodes statiques abstraites dans les classes et des méthodes statiques dans les interfaces

Créé le 12 mars 2017  ·  98Commentaires  ·  Source: microsoft/TypeScript

Comme suite #2947, qui autorise le modificateur abstract sur les déclarations de méthode mais l'interdit sur les déclarations de méthodes statiques, je suggère d'étendre cette fonctionnalité aux déclarations de méthodes statiques en autorisant le modificateur abstract static sur les déclarations de méthode .

Le problème connexe concerne le modificateur static sur la déclaration des méthodes d'interface, qui n'est pas autorisé.

1. Le problème

1.1. Méthodes statiques abstraites dans les classes abstraites

Dans certains cas d'utilisation de la classe abstraite et de ses implémentations, je peux avoir besoin d'avoir des valeurs dépendantes de la classe (et non dépendantes de l'instance), auxquelles on doit accéder dans le contexte de la classe enfant (pas dans le contexte d'un objet), sans création d'un objet. La fonctionnalité qui permet de faire cela est le modificateur static sur la déclaration de la méthode.

Par exemple (exemple 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'

Mais dans certains cas, j'ai également besoin d'accéder à cette valeur lorsque je sais seulement que la classe d'accès est héritée de AbstractParentClass, mais je ne sais pas à quelle classe enfant spécifique j'accède. Je veux donc être sûr que chaque enfant de AbstractParentClass a cette méthode statique.

Par exemple (exemple 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'.
    }
}

En conséquence, le compilateur décide qu'une erreur s'est produite et affiche le message : La propriété 'getSomeClassDependentValue' n'existe pas sur le type 'typeof AbstractParentClass'.

1.2. Méthodes statiques dans les interfaces

Dans certains cas, la logique d'interface implique que les classes d'implémentation doivent avoir une méthode statique, qui a la liste prédéterminée de paramètres et renvoie la valeur de type exact.

Par exemple (exemple 3) :

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

Lors de la compilation de ce code, une erreur se produit : le modificateur 'static' ne peut pas apparaître sur un membre de type.

2. La solution

La solution aux deux problèmes (1.1 et 1.2) est d'autoriser le modificateur abstract sur les déclarations de méthodes statiques dans les classes abstraites et le modificateur static dans les interfaces.

3. Implémentation JS

L'implémentation de cette fonctionnalité en JavaScript doit être similaire à l'implémentation d'interfaces, de méthodes abstraites et de méthodes statiques.

Cela signifie que:

  1. La déclaration de méthodes statiques abstraites dans une classe abstraite ne doit pas affecter la représentation de la classe abstraite dans le code JavaScript.
  2. La déclaration de méthodes statiques dans l'interface ne doit pas affecter la représentation de l'interface dans le code JavaScript (elle n'est pas présente).

Par exemple, ce code TypeScript (exemple 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';
    }
}

doit être compilé dans ce code JS :

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

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

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

4. Points pertinents

  • La déclaration d'une méthode statique abstraite d'une classe abstraite doit être marquée avec le modificateur abstract static
  • L'implémentation de la méthode statique abstraite d'une classe abstraite doit être marquée avec le modificateur abstract static ou static
  • La déclaration de la méthode statique d'une interface doit être marquée avec le modificateur static
  • L'implémentation de la méthode statique d'une interface doit être marquée avec le modificateur static

Toutes les autres propriétés du modificateur abstract static doivent être héritées des propriétés des modificateurs abstract et static .

Toutes les autres propriétés du modificateur de méthode d'interface static doivent être héritées des méthodes d'interface et des propriétés du modificateur static .

5. Liste de contrôle des fonctionnalités linguistiques

  • Syntactique

    • _Quelle est la grammaire de cette fonctionnalité ?_ - La grammaire de cette fonctionnalité est le modificateur abstract static d'une méthode de classe abstraite et le modificateur static d'une méthode d'interface.

    • _Y a-t-il des implications pour la rétrocompatibilité JavaScript ? Si oui, sont-ils suffisamment atténués ? _ - Il n'y a aucune implication pour la rétrocompatibilité JavaScript.

    • _Cette syntaxe interfère-t-elle avec les modifications ES6 ou ES7 plausibles ?_ - Cette syntaxe n'interfère pas avec les modifications ES6 ou ES7 plausibles.

  • Sémantique

    • _Qu'est-ce qu'une erreur sous la fonctionnalité proposée ?_ - Il y a maintenant des erreurs lors de la compilation du modificateur abstract static d'une méthode de classe abstraite et du modificateur static d'une méthode d'interface.

    • _Comment la fonctionnalité affecte-t-elle les relations de sous-type, de supertype, d'identité et d'assignabilité ?_ - La fonctionnalité n'a pas d'impact sur les relations de sous-type, de supertype, d'identité et d'assignabilité.

    • _Comment la fonctionnalité interagit-elle avec les génériques ?_ - La fonctionnalité n'interagit pas avec les génériques.

  • Émettre

    • _Quels sont les effets de cette fonctionnalité sur l'émission JavaScript ?_ - Il n'y a aucun effet de cette fonctionnalité sur l'émission JavaScript.

    • _Est-ce que cela émet correctement en présence de variables de type 'any' ?_ - Oui.

    • _Quels sont les impacts sur le fichier de déclaration (.d.ts) émis ?_ - Il n'y a aucun impact sur le fichier de déclaration.

    • _Cette fonctionnalité fonctionne-t-elle bien avec les modules externes ?_ - Oui.

  • Compatibilité

    • _Est-ce un changement majeur par rapport au compilateur 1.0 ?_ - Probablement oui, le compilateur 1.0 ne pourra pas compiler le code implémentant cette fonctionnalité.

    • _Est-ce un changement radical du comportement de JavaScript ?_ - Non.

    • _Est-ce une implémentation incompatible d'une future fonctionnalité JavaScript (c'est-à-dire ES6/ES7/plus tard) ?_ - Non.

  • Autre

    • _La fonctionnalité peut-elle être implémentée sans affecter négativement les performances du compilateur ?_ - Probablement oui.

    • _Quel impact a-t-il sur les scénarios d'outillage, tels que la complétion des membres et l'aide à la signature dans les éditeurs ?_ - Il n'a probablement aucun impact de ce type.

Awaiting More Feedback Suggestion

Commentaire le plus utile

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

Je veux forcer l'implémentation de la méthode de désérialisation statique dans les sous-classes de Serializable.
Existe-t-il une solution de contournement pour mettre en œuvre un tel comportement?

Tous les 98 commentaires

Les méthodes d'interface statiques n'ont généralement pas de sens, voir #13462

Tenir à l'écart jusqu'à ce que nous entendions plus de commentaires à ce sujet.

Une question majeure que nous nous sommes posée lors de cet examen : qui est autorisé à appeler une méthode abstract static ? Vraisemblablement, vous ne pouvez pas invoquer AbstractParentClass.getSomeClassDependentValue directement. Mais pouvez-vous invoquer la méthode sur une expression de type AbstractParentClass ? Si oui, pourquoi cela devrait-il être autorisé ? Si non, à quoi sert la fonctionnalité ?

Les méthodes d'interface statiques n'ont généralement pas de sens, voir #13462

Dans la discussion sur #13462, je n'ai pas vu pourquoi les méthodes d'interface static sont insensées. J'ai seulement vu que leur fonctionnalité peut être implémentée par d'autres moyens (ce qui prouve qu'ils ne sont pas insensés).

D'un point de vue pratique, l'interface est une sorte de spécification - un certain ensemble de méthodes qui sont obligatoires pour leur implémentation dans une classe qui implémente cette interface. L'interface ne définit pas seulement la fonctionnalité fournie par un objet, c'est aussi une sorte de contrat.

Donc, je ne vois aucune raison logique pour laquelle class peut avoir la méthode static et interface pas.

Si nous suivons le point de vue selon lequel tout ce qui peut déjà être implémenté dans le langage n'a pas besoin d'améliorations de la syntaxe et d'autres choses (à savoir, cet argument était l'un des principaux points de la discussion de # 13462), alors guidé par ce point de vue, nous pouvons décider que le cycle while est redondant car il peut être implémenté en utilisant for et if ensemble. Mais nous n'allons pas nous passer de while .

Une question majeure que nous nous sommes posée lors de cet examen : qui est autorisé à appeler une méthode abstract static ? Vraisemblablement, vous ne pouvez pas invoquer AbstractParentClass.getSomeClassDependentValue directement. Mais pouvez-vous invoquer la méthode sur une expression de type AbstractParentClass ? Si oui, pourquoi cela devrait-il être autorisé ? Si non, à quoi sert la fonctionnalité ?

Bonne question. Puisque vous réfléchissiez à ce problème, pourriez-vous s'il vous plaît partager vos idées à ce sujet ?

Il me vient seulement à l'esprit qu'au niveau du compilateur, le cas d'un appel direct de AbstractParentClass.getSomeClassDependentValue ne sera pas suivi (car il ne peut pas être suivi) et l'erreur d'exécution JS se produira. Mais je ne suis pas sûr que cela soit cohérent avec l'idéologie TypeScript.

J'ai seulement vu que leur fonctionnalité peut être implémentée par d'autres moyens (ce qui prouve qu'ils ne sont pas insensés).

Ce n'est pas parce que quelque chose est implémentable que cela a du sens. 😉

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

Je veux forcer l'implémentation de la méthode de désérialisation statique dans les sous-classes de Serializable.
Existe-t-il une solution de contournement pour mettre en œuvre un tel comportement?

Quelle est la dernière version à ce sujet ? J'ai juste essayé d'écrire une propriété statique abstraite d'une classe abstraite et j'ai été vraiment surpris quand ce n'était pas autorisé.

Ce que @patrik-zielinski93 a dit. Après quelques années de projets en PHP que nous convertissons en TS, nous voulons des méthodes statiques dans les interfaces.

Un cas d'utilisation très courant avec lequel cela vous aidera est les composants React, qui sont des classes avec des propriétés statiques telles que displayName , propTypes et defaultProps .

En raison de cette limitation, les typages pour React incluent actuellement deux types : une classe Component #$3$#$ et une interface ComponentClass comprenant la fonction constructeur et les propriétés statiques.

Pour vérifier complètement le type d'un composant React avec toutes ses propriétés statiques, il faut utiliser les deux types.

Exemple sans ComponentClass : les propriétés statiques sont ignorées

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

Exemple avec ComponentClass : les propriétés statiques sont vérifiées

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

Je soupçonne que beaucoup de gens n'utilisent pas actuellement ComponentClass , ignorant que leurs propriétés statiques ne sont pas vérifiées.

Problème connexe : https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16967

Y a-t-il des progrès à ce sujet? Ou une solution de contournement pour les constructeurs sur des classes abstraites ?
Voici un exemple :

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!

Convenu qu'il s'agit d'une fonctionnalité extrêmement puissante et utile. À mon avis, cette caractéristique est ce qui fait des classes des « citoyens de première classe ». L'héritage des méthodes de classe/statique peut avoir et a du sens, en particulier pour le modèle d'usine de méthode statique, qui a été appelé ici par d'autres affiches à plusieurs reprises. Ce modèle est particulièrement utile pour la désérialisation, qui est une opération fréquemment effectuée dans TypeScript. Par exemple, il est parfaitement logique de vouloir définir une interface qui fournit un contrat indiquant que tous les types d'implémentation sont instanciables à partir de JSON.

Ne pas autoriser les méthodes de fabrique abstraites statiques oblige l'implémenteur à créer des classes de fabrique abstraites à la place, ce qui double inutilement le nombre de définitions de classe. Et, comme d'autres affiches l'ont souligné, il s'agit d'une fonctionnalité puissante et réussie implémentée dans d'autres langages, tels que PHP et Python.

Nouveau sur Typescript, mais je suis également surpris que cela ne soit pas autorisé par défaut et que tant de personnes essaient de justifier de ne pas ajouter la fonctionnalité avec :

  1. TS n'a pas besoin de cette fonctionnalité, car vous pouvez toujours accomplir ce que vous essayez de faire par d'autres moyens (ce qui n'est un argument valable que si vous fournissez un exemple d'une manière très objectivement meilleure de faire quelque chose, dont j'ai vu très peu)
  2. Ce n'est pas parce qu'on peut qu'on doit. Génial : mais les gens publient des exemples spécifiques de la façon dont cela serait utile/bénéfique. Je ne vois pas en quoi ça pourrait faire mal de le permettre.

Un autre cas d'utilisation simple : (moyen idéal, qui ne fonctionne pas)

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

Au lieu de cela, l'instance de liste finit par exposer directement le type d'instance, qui n'est pas pertinent pour l'instance de liste elle-même et doit être accessible via le constructeur. Se sent sale. Ce serait beaucoup plus sale si nous parlions d'un modèle d'enregistrement, dans lequel spécifier un ensemble de valeurs par défaut via le même type de mécanisme, etc.

J'étais vraiment excité d'utiliser de vraies classes abstraites dans un langage après avoir été en ruby/javascript pendant si longtemps, seulement pour finir consterné par les restrictions d'implémentation - L'exemple ci-dessus n'était que mon premier exemple de le rencontrer, même si je peux penser à beaucoup d'autres cas d'utilisation où cela serait utile. Principalement, juste comme un moyen de créer des DSL/configuration simples dans le cadre de l'interface statique, en s'assurant qu'une classe spécifie un objet de valeurs par défaut ou quoi que ce soit. - Et vous vous dites peut-être que c'est à ça que servent les interfaces. Mais pour quelque chose de simple comme celui-ci, cela n'a pas vraiment de sens et finit par rendre les choses plus compliquées qu'elles ne devraient l'être (la sous-classe aurait besoin d'étendre la classe abstraite et d'implémenter une interface, rend la dénomination des choses plus compliquée, etc).

J'ai eu cette exigence similaire pour mon projet à deux reprises. Les deux étaient liés pour garantir que toutes les sous-classes fournissent des implémentations concrètes d'un ensemble de méthodes statiques. Mon scénario est décrit ci-dessous :

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

J'espère que cela peut expliquer mes exigences. Merci.

Pour ceux qui recherchent une solution de contournement , vous pouvez utiliser ce décorateur :

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

puisque nous appelons via un paramètre de type, la classe de base réelle avec sa méthode de fabrique hypothétique abstract static ne sera jamais impliquée. Les types des sous-classes sont structurellement liés lorsque le paramètre de type est instancié.

Ce que je ne vois pas dans cette discussion, ce sont des cas où l'implémentation est invoquée au moyen du type de la classe de base par opposition à un type synthétisé.

Il est également pertinent de considérer que, contrairement à des langages tels que C # où les membres abstract sont en fait _écrasés_ par leurs implémentations dans les classes dérivées, les implémentations de membres JavaScript, instance ou autre, ne remplacent jamais les nombres hérités, mais plutôt les _ombre_.

Mes 5 cents ici, du point de vue de l'utilisateur, sont :

Pour le cas des interfaces, il faut autoriser le modificateur _static_

Les interfaces définissent des contrats, à remplir en implémentant des classes. Cela signifie que les interfaces sont des abstractions à un niveau supérieur aux classes.

En ce moment, ce que je peux voir, c'est que les classes peuvent être plus expressives que les interfaces, de manière à ce que nous puissions avoir une méthode statique dans les classes, mais nous ne pouvons pas l'appliquer, à partir de la définition du contrat elle-même.

À mon avis, cela semble mal et gênant.

Existe-t-il des connaissances techniques rendant cette fonctionnalité de langage difficile ou impossible à mettre en œuvre ?

à votre santé

Les interfaces définissent des contrats, à remplir en implémentant des classes. Cela signifie que les interfaces sont des abstractions à un niveau supérieur aux classes.

Les classes ont deux interfaces, deux contrats d'implémentation, et il n'y a pas moyen d'y échapper.
Il y a des raisons pour lesquelles des langages comme C # n'ont pas non plus de membres statiques pour les interfaces. Logiquement, les interfaces sont la surface publique d'un objet. Une interface décrit la surface publique d'un objet. Cela signifie qu'il ne contient rien qui ne soit pas là. Sur les instances de classes, les méthodes statiques ne sont pas présentes sur l'instance. Ils n'existent que sur la fonction classe/constructeur, ils ne doivent donc être décrits que dans cette interface.

Sur les instances de classes, les méthodes statiques ne sont pas présentes sur l'instance.

Pourriez-vous préciser ceci? Ce n'est pas du C#

Ils n'existent que sur la fonction classe/constructeur, ils ne doivent donc être décrits que dans cette interface.

Que nous l'avons eu et c'est ce que nous voulons changer.

Les interfaces définissent des contrats, à remplir en implémentant des classes. Cela signifie que les interfaces sont des abstractions à un niveau supérieur aux classes.

Je suis entièrement d'accord là-dessus. Voir l'exemple d'interface Json

Bonjour @kitsonk , pourriez-vous nous en dire plus sur :

Les classes ont deux interfaces, deux contrats d'implémentation, et il n'y a pas moyen d'y échapper.

Je n'ai pas compris cette partie.

Logiquement, les interfaces sont la surface publique d'un objet. Une interface décrit la surface publique d'un objet. Cela signifie qu'il ne contient rien qui ne soit pas là.

Je suis d'accord. Je ne vois aucune contradiction avec ce que j'ai dit. J'en ai même dit plus. J'ai dit qu'une interface est un contrat pour une classe à remplir.

Sur les instances de classes, les méthodes statiques ne sont pas présentes sur l'instance. Ils n'existent que sur la fonction classe/constructeur, ils ne doivent donc être décrits que dans cette interface.

Je ne suis pas sûr d'avoir bien compris, ce qu'il dit est clair, mais pas pourquoi vous le dites. Je peux expliquer pourquoi je pense que ma déclaration est toujours valable. Je passe tout le temps des interfaces en tant que paramètres de mes méthodes, cela signifie que j'ai accès aux méthodes d'interface, veuillez noter que je n'utilise pas ici les interfaces pour définir la structure des données mais pour définir des objets concrets qui sont créés/hydratés ailleurs . Alors quand j'ai :

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

Dans ce corps de méthode, je peux en effet utiliser les méthodes account . Ce que j'aimerais pouvoir faire, c'est pouvoir utiliser des méthodes statiques de SalesRepresentativeInterface qui ont déjà été appliquées pour être implémentées dans la classe que je reçois dans account . Peut-être que j'ai une idée très simpliste ou complètement fausse sur la façon d'utiliser la fonctionnalité.

Je pense qu'autoriser le modificateur static me permettra de faire quelque chose comme : SalesRepresentativeInterface.staticMethodCall()

Ai-je tort ?

à votre santé

@davidmpaz : votre syntaxe n'est pas tout à fait correcte, mais proche. Voici un exemple de modèle d'utilisation :

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

Malheureusement, pour que cela fonctionne, nous avons besoin de deux fonctionnalités de TypeScript : (1) les méthodes statiques dans les interfaces et les méthodes statiques abstraites ; (2) la possibilité d'utiliser typeof comme indice de type avec des classes génériques.

@davidmpaz

Je n'ai pas compris cette partie.

Les classes ont deux interfaces. La fonction constructeur et le prototype d'instance. Le mot clé class est essentiellement du sucre pour cela.

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

Pourriez-vous préciser ceci? Ce n'est pas du C#

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

const foo = new Foo();

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

Il n'y a pas .baz() sur les instances de Foo . .baz() n'existe que sur le constructeur.

Du point de vue du type, vous pouvez référencer/extraire ces deux interfaces. Foo fait référence à l'interface de l'instance publique et typeof Foo fait référence à l'interface du constructeur public (qui inclurait les méthodes statiques).

Pour faire suite à l'explication de @kitsonk , voici l'exemple ci-dessus réécrit pour obtenir ce que vous voulez :

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

Notez ici que :

  • La clause implements n'est pas vraiment nécessaire, chaque fois que vous utilisez la classe, une vérification structurelle est effectuée et la classe sera validée pour correspondre à l'interface nécessaire.
  • vous n'avez pas besoin typeof pour obtenir le type de constructeur, assurez-vous simplement que votre T correspond à ce que vous avez l'intention de capturer

@mhegazy : vous avez dû supprimer mon appel toJSON dans l'interface JSONSerializable. Bien que mon exemple de fonction simple makeInstance n'appelle que fromJSON , il est très courant de vouloir instancier un objet, puis de l'utiliser. Vous avez supprimé ma capacité à effectuer des appels sur ce qui est renvoyé par makeInstance , car je ne sais pas réellement ce que T est à l'intérieur makeInstance (particulièrement pertinent si makeInstance est une méthode de classe utilisée en interne pour créer une instance, puis utilisez-la). Bien sûr, je pourrais faire ceci :

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

Et maintenant, je sais que mon T aura toutes les méthodes disponibles sur JSONSerializer . Mais c'est trop verbeux et difficile à raisonner (attendez, vous passez ImplementsJSONSerializer , mais ce n'est pas un JSONSerializable , n'est-ce pas ? Attendez, vous tapez du canard ??) . La version la plus facile à raisonner est la suivante :

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

Mais comme je l'ai souligné dans un commentaire précédent, nous devons maintenant créer une classe d'usine pour chaque classe que nous voulons instancier, ce qui est encore plus verbeux que l'exemple de typage canard. C'est certainement un modèle réalisable en Java tout le temps (et je suppose également que C#). Mais c'est inutilement verbeux et redondant. Tout ce passe-partout disparaît si les méthodes statiques sont autorisées dans les interfaces et que les méthodes statiques abstraites sont autorisées dans les classes abstraites.

pas besoin de la classe supplémentaire. les classes peuvent avoir static membres.. vous n'avez simplement pas besoin de la clause implements . dans un système de type nominal, vous en avez vraiment besoin pour affirmer la relation "est un" sur votre classe. dans un système de type structurel, vous n'en avez pas vraiment besoin. la relation "est un" est de toute façon vérifiée à chaque utilisation, vous pouvez donc supprimer en toute sécurité la clause implements et ne pas perdre la sécurité.

donc en d'autres termes, vous pouvez avoir une classe qui a un fromJSON statique et dont les instances ont un toJSON :

interface JSONSerializer {
    toJSON(): any;
}

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

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

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


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

@mhegazy J'ai déjà souligné cela comme une option viable. Voir mon premier exemple de "duck-typing". J'ai soutenu qu'il est inutilement verbeux et difficile à raisonner. J'ai alors affirmé que la version sur laquelle il est plus facile de raisonner (classes d'usine) est encore plus verbeuse.

Personne ne conteste qu'il est possible de contourner le manque de méthodes statiques dans les interfaces. En fait, je dirais qu'il existe une solution de contournement plus élégante en utilisant des styles fonctionnels plutôt que des classes d'usine, bien que ce soit ma préférence personnelle.

Mais, à mon avis, la manière la plus propre et la plus simple de raisonner pour implémenter ce modèle exceptionnellement courant consiste à utiliser des méthodes statiques abstraites. Ceux d'entre nous qui ont appris à aimer cette fonctionnalité dans d'autres langages (Python, PHP) manquent de l'avoir dans TypeScript. Merci pour votre temps.

J'ai alors affirmé que la version sur laquelle il est plus facile de raisonner (classes d'usine)

Je ne suis pas sûr d'être d'accord pour dire que c'est plus facile de raisonner.

Mais, à mon avis, la manière la plus propre et la plus simple de raisonner pour implémenter ce modèle exceptionnellement courant consiste à utiliser des méthodes statiques abstraites. Ceux d'entre nous qui ont appris à aimer cette fonctionnalité dans d'autres langages (Python, PHP) manquent de l'avoir dans TypeScript.

Et ce problème suit l'ajout de cet élément. Je voulais juste m'assurer que les futurs visiteurs de ce fil comprennent que c'est faisable aujourd'hui sans clause implements .

@kitsonk, j'ai raté ça.

Les gars, j'ai donc comme LocationModule, qui devrait avoir une méthode pour obtenir des options à stocker dans la base de données, à partir de laquelle il devrait se recréer.
Chaque LocationModule a ses propres options avec le type de loisirs.

Alors maintenant, je dois créer une usine avec des génériques pour les loisirs et même alors, je n'ai pas de vérification du temps de compilation. Vous savez, battre le but ici.

Au début, j'étais comme "bien pas de statique pour les interfaces, alors restons avec la classe abstraite MAIS même cela serait sale, car maintenant je dois changer partout dans mon type de code de l'interface à la classe abstraite pour que le vérificateur reconnaisse les méthodes que je recherche .

Ici:

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

Et puis je tombe sur bien, le statique ne peut pas être avec l'abstrait. Et cela ne peut pas être dans l'interface.

Les gars, sérieusement, ne protégeons-nous pas le fait de ne pas mettre cela en œuvre uniquement pour ne pas le mettre en œuvre ?

J'aime passionnément TypeScript. Comme un fou je veux dire. Mais il y a toujours mais.

Au lieu d'avoir une implémentation forcée de la méthode statique, je devrais revérifier comme je le faisais avec tout dans le bon vieux JavaScript.

L'architecture est déjà pleine de modèles, de décisions intéressantes, etc., mais certaines d'entre elles ne servent qu'à surmonter les problèmes liés à l'utilisation de choses aussi simples que la méthode statique d'interface.

Mon usine devra vérifier l'existence de la méthode statique lors de l'exécution. Comment c'est?

Ou encore mieux :

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
  dontForgetToHaveStaticMethodForRecreation();
}

Si vous utilisez l'objet de classe de manière polymorphe, ce qui est la seule raison pour laquelle il implémente une interface, peu importe si la ou les interfaces qui spécifient les exigences pour le côté statique de la classe sont référencées syntaxiquement par la classe elle-même. car il sera vérifié sur le site d'utilisation et émettra une erreur de conception si l'interface ou les interfaces requises ne sont pas implémentées.

@malina-kirn

Voir mon premier exemple de "duck-typing".

Ce problème n'a aucune incidence sur l'utilisation ou non du duck typing. TypeScript est typé canard.

@aluanhaddad Votre implication n'est pas correcte. Je peux comprendre votre argument selon lequel la fonction principale implements ISomeInterface est d'utiliser l'objet de classe de manière polymorphe. Cependant, il peut être important que la classe fasse référence à la ou aux interfaces décrivant la forme statique de l'objet de classe.

Vous impliquez que l'inférence de type implicite et les erreurs de site d'utilisation couvrent tous les cas d'utilisation.
Considérez cet exemple :

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

Dans l'exemple ci-dessus, une déclaration de type explicite est requise ; ne pas décrire la forme de l'objet de classe (côté statique) entraîne une erreur de conception sur le site d'utilisation même si les valeurs réelles sont conformes à la forme attendue.

De plus, l'absence de moyen de référencer la ou les interfaces décrivant le côté statique, nous laisse la seule option étant de déclarer explicitement le type sur chaque membre individuel ; puis de répéter cela sur chaque classe.

Sur la base de mon post précédent, je pense que le modificateur static dans les interfaces est une très bonne solution pour certains cas d'utilisation qui doivent être résolus. Je m'excuse pour la taille de ce commentaire, mais il y a beaucoup de choses que je veux illustrer.

1) Il y a des moments où nous devons être capables de décrire explicitement la forme du côté statique d'un objet de classe, comme dans mon exemple ci-dessus.
2) Il y a des moments où la vérification de la forme sur le site de définition est hautement souhaitable, et des moments comme dans l'exemple de @OliverJAsh où le site d'utilisation est dans un code externe qui ne sera pas vérifié et vous devez vérifier la forme sur le site de définition .

En ce qui concerne le numéro 2, de nombreux messages que j'ai lus suggèrent de vérifier la forme car le site d'utilisation est plus que suffisant. Mais dans les cas où le site d'utilisation se trouve dans un module galaxie très, très loin... ou lorsque le site d'utilisation se trouve dans un emplacement externe qui ne sera pas vérifié, ce n'est évidemment pas le cas.

D'autres messages suggèrent #workarounds pour vérifier la forme sur le site de définition. Bien que ces redoutables solutions de contournement vous permettent de vérifier la forme sur le site de définition (dans certains cas), il existe des problèmes :

  • Ils ne résolvent pas le numéro 1 ci-dessus, vous devez toujours déclarer explicitement le type sur chaque membre de chaque classe.
  • Dans de nombreux cas, ils manquent de clarté, d'élégance et de lisibilité. Ils ne sont pas toujours faciles à utiliser, pas intuitifs et peuvent prendre un certain temps pour comprendre qu'ils sont possibles.
  • Ils ne fonctionnent pas dans tous les cas et trouver #workaroundsToMakeTheWorkaroundsWork n'est ni amusant... ni productif... mais surtout pas amusant.

Voici un exemple de la solution de contournement que j'ai vue récemment pour forcer la vérification de la forme sur le site de définition...

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

Essayez maintenant d'utiliser cette solution de contournement avec une classe abstraite lancée dans

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

Nous allons je suppose que nous allons travailler autour de ça aussi... soupir

...

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;

Mais attendez, il y a plus ! Voyons ce qui se passe si nous utilisons des génériques...

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

À ce stade, si vous avez un site d'utilisation dans le code qui sera vérifié par tapuscrit, vous devriez probablement mordre la balle et vous y fier même s'il est vraiment loin.

Si vous utilisez le site est externe, convainquez la communauté/les responsables d'accepter le modificateur static pour les membres de l'interface. Et en attendant, vous aurez besoin d'une autre solution de contournement. Ce n'est pas sur le site de définition mais je pense que vous pouvez utiliser une version modifiée d'une solution de contournement décrite dans # 8328 pour y parvenir. Voir la solution de contournement dans le commentaire de @mhegazy le 16 mai 2016 avec l'aimable autorisation de @RyanCavanaugh

Pardonnez-moi si je manque des points clés. Ma compréhension de l'hésitation à prendre en charge static pour les membres de l'interface est principalement une aversion à utiliser le même mot-clé interface + implements pour décrire à la fois la forme du constructeur et la forme de l'instance.

Permettez-moi de préfacer l'analogie suivante en disant que j'aime ce que propose le dactylographié et que j'apprécie grandement ceux qui l'ont développé et la communauté qui consacre beaucoup de réflexion et d'efforts pour traiter la pléthore de demandes de fonctionnalités ; font de leur mieux pour mettre en œuvre les fonctionnalités qui conviennent le mieux.

Mais, l'hésitation dans ce cas m'apparaît comme un désir de préserver la « pureté conceptuelle » au détriment de l'utilisabilité. Encore désolé si cette analogie est loin de la base:

Cela rappelait le passage de Java à C# . Quand j'ai vu pour la première fois le code C# faire ça
C# var something = "something"; if(something == "something") { ... }
les sonnettes d'alarme ont commencé à retentir dans ma tête, le monde se terminait, ils devaient utiliser "something".Equals(something) , etc.

== est pour référence égale ! Vous avez un .Equals(...) séparé pour la comparaison de chaînes...
et le littéral de chaîne doit venir en premier afin que vous n'obteniez pas une référence nulle appelant .Equals(...) sur une variable nulle...
et... et... hyperventilation

Et puis, après quelques semaines d'utilisation de C# , j'ai réalisé à quel point il était plus facile à utiliser grâce à ce comportement. Même si cela signifiait abandonner la distinction claire entre les deux, cela améliore tellement la convivialité que cela en vaut la peine.

C'est ce que je pense du modificateur static pour les membres de l'interface. Cela nous permettra de continuer à décrire la forme de l'instance comme nous le faisons déjà, nous permettra de décrire la forme de la fonction constructeur, ce que nous ne pouvons faire qu'avec de mauvaises solutions de contournement au cas par cas, et nous permettra de faire les deux de manière relativement propre et facile. façon. Une énorme amélioration de la convivialité, IMO.

Je vais peser sur abstract static dans le prochain commentaire...

Les interfaces nécessitent toujours une nouvelle spécification des types de membres lors de l'implémentation des classes. (l'inférence des signatures des membres dans les classes d'implémentation _explicitement_ est une fonctionnalité distincte, non encore prise en charge).

La raison pour laquelle vous obtenez des erreurs n'est pas liée à cette proposition. Pour contourner ce problème, vous devez utiliser le modificateur readonly sur l'implémentation des membres de classe, statiques ou autres, qui doivent être de types littéraux ; sinon, string sera déduit.

Encore une fois, le mot-clé implements n'est qu'une spécification d'intention, il n'influence pas la compatibilité structurelle des types, ni n'introduit de typage nominal. Cela ne veut pas dire que cela ne pourrait pas être utile, mais cela ne change pas les types.

Donc abstract static ... Ça n'en vaut pas la peine. Trop de problèmes.

Vous auriez besoin d'une syntaxe nouvelle et complexe pour déclarer implicitement ce qui sera déjà vérifié sur le site d'utilisation (si le site d'utilisation sera vérifié par le compilateur dactylographié).

Si le site d'utilisation est externe, alors ce dont vous avez vraiment besoin, c'est static membres d'une interface... Désolé d'avoir branché... et bonne nuit !

@aluanhaddad Vrai, mais même si l'inférence des signatures des membres dans les classes d'implémentation explicites était une fonctionnalité prise en charge, nous manquons de moyens propres de déclarer les signatures des membres pour le côté statique de la classe.

Le point que j'essayais d'exprimer était que nous manquions de moyens pour explicitly déclarer la structure attendue du côté statique de la classe et il y a des cas où cela compte (ou comptera pour prendre en charge les fonctionnalités)

Je voulais principalement réfuter cette partie de votre commentaire "peu importe si la ou les interfaces qui spécifient les exigences pour le côté statique de la classe sont référencées syntaxiquement".

J'essayais d'utiliser l'inférence de type comme exemple de la raison pour laquelle cela serait important avec un support futur.
En référençant syntaxiquement l'interface ici let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' } nous n'avons pas à déclarer explicitement le type comme 'Oui' | 'Non' 100 fois.

Pour obtenir la même inférence de type pour le côté statique d'une classe (à l'avenir), nous aurons besoin d'un moyen de référencer syntaxiquement la ou les interfaces.

Après avoir lu votre dernier commentaire, je pense que ce n'était pas un exemple aussi clair que je l'espérais. Un meilleur exemple est peut-être lorsque le site d'utilisation du côté statique de la classe se trouve dans un code externe qui ne sera pas vérifié par le compilateur dactylographié. Dans un tel cas, nous devons actuellement compter sur des solutions de contournement qui créent essentiellement un site d'utilisation artificiel, offrent une convivialité/lisibilité médiocre et ne fonctionnent pas dans de nombreux cas.

Au prix d'un peu de verbosité, vous pouvez atteindre un niveau décent de sécurité de type :

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

D'accord, cela devrait être autorisé. Le cas d'utilisation simple suivant bénéficierait de méthodes statiques abstraites :

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 Je ne pense pas que quiconque l'ait encore mentionné exactement (bien que j'aurais pu le manquer dans ma lecture rapide), mais en réponse à :

Une question majeure que nous avions en considérant cela : qui est autorisé à appeler une méthode statique abstraite ? Vraisemblablement, vous ne pouvez pas appeler directement AbstractParentClass.getSomeClassDependentValue. Mais pouvez-vous invoquer la méthode sur une expression de type AbstractParentClass ? Si oui, pourquoi cela devrait-il être autorisé ? Si non, à quoi sert la fonctionnalité ?

Cela pourrait être résolu en implémentant une partie de #3841. En parcourant ce problème, la principale objection soulevée semble être que les types de fonctions constructeurs des classes dérivées ne sont souvent pas compatibles avec les fonctions constructeurs de leurs classes de base. Cependant, la même préoccupation ne semble pas s'appliquer à d'autres méthodes ou champs statiques, car TypeScript vérifie déjà que les statiques remplacées sont compatibles avec leurs types dans la classe de base.

Donc, ce que je proposerais, c'est de donner T.constructor le type Function & {{ statics on T }} . Cela permettrait aux classes abstraites qui déclarent un champ abstract static foo d'y accéder en toute sécurité via this.constructor.foo sans causer de problèmes d'incompatibilités de constructeur.

Et même si l'ajout automatique des statiques à T.constructor n'est pas implémenté, nous serions toujours en mesure d'utiliser les propriétés abstract static dans la classe de base en déclarant manuellement "constructor": typeof AbstractParentClass .

Je pense que beaucoup de gens s'attendent à ce que l' exemple @ patryk-zielinski93 fonctionne. Au lieu de cela, nous devrions utiliser des solutions de contournement contre-intuitives, verbeuses et cryptiques. Puisque nous avons déjà des classes et des membres statiques "sucrés à la syntaxe", pourquoi ne pouvons-nous pas avoir un tel sucre dans le système de type ?

Voici ma douleur :

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

Peut-être pourrions-nous à la place/en parallèle introduire une clause static implements ? par exemple

interface Foo {
    bar(): number;
}

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

Cela a l'avantage d'être rétrocompatible avec la déclaration d'interfaces séparées pour les membres statiques et d'instance, ce qui semble être la solution de contournement actuelle de choix, si je lis correctement ce fil. C'est aussi une fonctionnalité subtilement différente que je pourrais voir être utile en soi, mais ce n'est pas la question.

( statically implements serait mieux, mais cela introduit un nouveau mot-clé. Discutable si cela en vaut la peine. Cela pourrait être contextuel, cependant.)

Peu importe à quel point j'ai essayé de comprendre les arguments de ceux qui sont sceptiques quant à la proposition d'OP, j'ai échoué.

Une seule personne (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) vous a répondu @RyanCavanaugh . J'ai réécrit le code d'OP pour le rendre un peu plus propre et aussi pour illustrer votre question et mes réponses :

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

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

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

Qui est autorisé à appeler une méthode statique abstraite ? Vraisemblablement, vous ne pouvez pas invoquer Base.foo directement

Si vous voulez dire ref # 1, alors oui, il ne devrait jamais être appelable, car il est abstrait et n'a même pas de corps.
Vous n'êtes autorisé à appeler que la réf # 2.

Mais pouvez-vous invoquer la méthode sur une expression de type Base ?
Si oui, pourquoi cela devrait-il être autorisé ?

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 est une erreur. Non, vous ne pouvez pas invoquer "foo" ici, car baz est censé être une __instance__ de Child1 ou Child2, et les instances n'ont pas de méthodes statiques

ref # 4 est (devrait être) correct. Vous pouvez (devriez pouvoir) invoquer la méthode statique foo ici, car baz est censé être le constructeur de Child1 ou Child2, qui étendent tous deux Base et doivent donc avoir implémenté foo().

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

Vous pouvez imaginer cette situation :

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

A quoi sert la fonctionnalité ?

Voir ref # 4 L'utilisation est lorsque nous ne savons pas laquelle des classes enfants nous recevons comme argument (ce peut être Child1 ou Child2), mais nous voulons appeler sa méthode statique et être sûr que cette méthode existe.
Je suis en train de réécrire le framework node.js, appelé AdonisJS. Il a été écrit en JS pur, donc je le transforme en TS. Je ne peux pas changer le fonctionnement du code, je ne fais qu'ajouter des types. L'absence de ces fonctionnalités me rend très triste.

ps Dans ce commentaire, pour des raisons de simplicité, j'ai écrit uniquement sur les classes abstraites, et n'ai pas mentionné les interfaces. Mais tout ce que j'ai écrit est applicable aux interfaces. Vous remplacez simplement la classe abstraite par l'interface, et tout sera correct. Il est possible que l'équipe TS, pour une raison quelconque (je ne sais pas pourquoi), ne veuille pas implémenter abstract static dans abstract class , mais ce serait bien de n'implémenter que static mot dans les interfaces, cela nous rendrait assez heureux, je suppose.

pps J'ai modifié les noms de classe et de méthode dans vos questions pour les conformer à mon code.

Quelques réflexions, basées sur les principaux arguments d'autres sceptiques :

"Non, cela ne doit pas être implémenté, mais vous pouvez déjà le faire de cette façon *écrit 15 fois plus de lignes de code*".

C'est pour ça que le sucre syntaxique est fait. Ajoutons un peu (de sucre) à TS. Mais non, nous ferions mieux de torturer le cerveau des développeurs qui tentent de percer les décorateurs et des dizaines de génériques au lieu d'ajouter un simple mot static aux interfaces. Pourquoi ne pas considérer ce static comme un autre sucre pour nous faciliter la vie ? De la même manière que ES6 ajoute class (qui devient omniprésent de nos jours). Mais non, soyons des nerds et faisons les choses à l'ancienne et de la "bonne" manière.

"Il existe deux interfaces pour les classes js : pour le constructeur et l'instance".

Ok, pourquoi ne pas nous donner un moyen de créer une interface pour le constructeur alors ? Cependant, ajouter simplement ce mot static est beaucoup plus facile et plus intuitif.

Et concernant ceci :

Tenir à l'écart jusqu'à ce que nous entendions plus de commentaires à ce sujet.

Plus d'un an s'est écoulé, et de nombreux commentaires ont été fournis, pendant combien de temps ce problème va-t-il être suspendu ? Quelqu'un peut-il répondre, s'il vous plaît.

Ce message peut sembler un peu dur ... Mais non, c'est une demande du gars qui aime TS et en même temps ne peut vraiment pas trouver une raison solide pour laquelle nous devrions utiliser des hacks laids, lors de la conversion de notre ancienne base de code JS en TS. En dehors de cela, un grand merci à l'équipe TS. TS est tout simplement merveilleux, il a changé ma vie et j'aime plus que jamais coder et mon travail... Mais ce problème empoisonne mon expérience...

Solution possible ?

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

Proposition : membres statiques dans les interfaces et les types, et membres abstraits dans les classes abstraites, v2

Cas d'utilisation

Types compatibles avec Fantasyland

````dactylographié
interface Applicative étend Appliquer {statique de (a : A) : Applicatif ; }

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

Type Serializable avec méthode statique deserialize

````dactylographié
interface sérialisable {
désérialisation(s) statique(s : chaîne) : sérialisable ;

serialize(): string;

}
````

Contrats en général

````dactylographié
Contrat d'interface {
création statique (x : nombre) : contrat ;
nouveau statique (x : nombre) : contrat ;
}

const factory1 = (c : contrat statique) : Contract => c.create(Math.random());
const factory2 = (C: static Contract): Contract => new C(Math.random());
const usine3 =(c: C): new C => c.create(Math.random());

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

Syntaxe

Méthodes et champs statiques

````dactylographié
interface sérialisable {
désérialisation(s) statique(s : chaîne) : sérialisable ;
}

type Sérialisable = {
désérialisation(s) statique(s : chaîne) : sérialisable ;
} ;

classe abstraite sérialisable {
désérialisation abstraite statique(s : chaîne) : sérialisable ;
}
````

Signatures de constructeur statiques

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

Suivi : signatures d'appel statiques

Discuter:

Comment les signatures d'appel statiques peuvent-elles être exprimées ?
Si nous allons les exprimer en ajoutant simplement le modificateur static avant la signature d'appel,
comment les distinguerons-nous de la méthode d'instance avec le nom 'static' ?
Pouvons-nous utiliser la même solution de contournement que pour les méthodes nommées 'new' ?
Dans ce cas, ce sera définitivement un changement de rupture.

L'opérateur de type static

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

L'opérateur de type new

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

Emplacements internes

L'emplacement interne [[Static]]

L'interface "statique" d'un type sera stockée dans le slot interne [[Static]] :

````dactylographié
// Le type
interface sérialisable {
désérialisation(s) statique(s : chaîne) : sérialisable ;
sérialiser : chaîne ;
}

// sera représenté en interne comme
// interface sérialisable {
// [[Statique]] : {
// [[Instance]] : sérialisable ; // Voir ci-dessous.
// désérialiser(s : chaîne) : sérialisable ;
// } ;
// sérialiser() : chaîne ;
// }
````

Par défaut ont le type never .

L'emplacement interne [[Instance]]

L'interface "Instance" du type sera stockée
dans l'emplacement interne [[Instance]] du type d'emplacement interne [[Static]] .

Par défaut ont le type never .

Sémantique

Sans l'opérateur static

Lorsqu'un type est utilisé comme type valeur,
l'emplacement interne [[Static]] sera supprimé :

````dactylographié
déclarer const sérialisable : sérialisable ;

type T = typede sérialisable ;
// { sérialiser() : chaîne ; }
````

Mais le type lui-même conserve l'emplacement interne [[Static]] :

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

Cessibilité

Les deux valeurs des types sensibles à l'électricité statique peuvent être attribuées aux éléments structurellement identiques
(sauf les [[Static]] , bien sûr) et vice versa.

L'opérateur static

| Associativité | Priorité |
| :-----------: | :-------------------------------- : |
| Droite | IDK, mais égal à celui new |

L'opérateur de type static renvoie le type de slot interne [[Static]] du type.
Il est un peu similaire à l'opérateur de type typeof ,
mais son argument doit être un type plutôt qu'une valeur.

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

L'opérateur de type typeof supprime également l'emplacement interne [[Instance]] :

````dactylographié
declare const SerializableImpl : static Serializable ;

type T = typeof SerializableImpl ;
// { désérialiser(s : chaîne) : sérialisable ; }
````

Cessibilité

Les deux valeurs des types sensibles à l'instance sont assignables aux éléments structurellement identiques
(à l'exception de la fente interne [[Instance]] , bien sûr) et vice versa.

L'opérateur new

| Associativité | Priorité |
| :-----------: | :-----------------------------------: |
| Droite | IDK, mais égal à celui static |

Les opérateurs new renvoient le type de slot interne [[Instance]] du type.
Il inverse efficacement l'opérateur static :

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

extends / implements sémantique

Une classe implémentant une interface avec un slot interne [[Static]] non par défaut
DOIT avoir un emplacement interne compatible [[Static]] .
Les contrôles de compatibilité doivent être identiques (ou similaires à)
vérifications régulières de compatibilité (instance).

````dactylographié
class SerializableImpl implémente Serializable {
désérialisation statique(s : chaîne) : SerializableImpl {
// La logique de désérialisation va ici.
}

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

serialize(): string {
    //
}

}
````

À FAIRE

  • [ ] Décidez quelle syntaxe doit être utilisée pour les signatures d'appel statiques.
    Peut-être sans casser les changements.
  • [ ] Existe-t-il un cas particulier avec les types conditionnels et l'opérateur infer ?
  • [ ] Modifications de la sémantique des membres de classe non abstraits. _Peut être en train de casser._

J'ai passé quelques heures à lire ce problème et d'autres problèmes pour une solution de contournement à mon problème, qui serait résolu par le modificateur statique ou les interfaces pour les classes. Je ne trouve pas une seule solution de contournement qui résout mon problème.

Le problème que j'ai est que je veux apporter une modification à une classe générée par code. Par exemple ce que je voudrais faire c'est :

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;

Je ne trouve pas une seule solution de contournement qui me permette de faire l'équivalent de cela, avec la version actuelle de TS. Me manque-t-il un moyen de résoudre ce problème actuellement?

Cela semble pertinent de publier ici en tant que cas d'utilisation pour lequel il n'y a apparemment aucune solution de contournement et certainement pas de solution de contournement simple.

Si vous souhaitez vérifier l'instance et le côté statique d'une classe par rapport aux interfaces, vous pouvez le faire comme ceci :

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

Nous sommes trop nombreux à perdre beaucoup trop d'heures de notre temps et d'autres à cela. Pourquoi n'est-ce pas une chose?!¿i!
Pourquoi les réponses sont-elles toutes d'énormes solutions de contournement pour quelque chose qui devrait être lisible, digeste et quelque chose qui est un 1-liner qui est simple. Il n'y a aucun moyen que je veuille que quelqu'un doive comprendre ce que mon code essaie de faire avec une solution de contournement hacky. Désolé d'encombrer davantage ce sujet avec quelque chose qui n'a pas beaucoup de valeur, mais c'est actuellement une énorme douleur et une perte de temps, donc je le trouve précieux pour moi, les autres en ce moment et ceux qui viendront plus tard chercher une réponse.

La chose avec n'importe quelle langue, à la fois naturelle et artificielle, qu'elle devrait naturellement évoluer pour être efficace et attrayante à utiliser. Finalement, les gens ont décidé d'utiliser des réductions de langage ("okay"=>"ok","going to" =>"gonna"), ont inventé de nouveaux mots ridicules, comme "selfie" et "google", ont redéfini l'orthographe avec l33tspeak et trucs, et même interdit certains mots, et, que vous vouliez les utiliser ou non, tout le monde comprend encore un peu ce qu'ils signifient, et certains d'entre nous les utilisent pour accomplir certaines tâches particulières. Et pour aucun de ceux-là, il ne peut y avoir de bonne raison, mais l'efficacité de certaines personnes dans certaines tâches, tout est question de nombre de personnes, qui les utilisent réellement. Le volume de cette conversation montre clairement que beaucoup de gens pourraient utiliser ce static abstract pour toutes les considérations qu'ils ont. Je suis venu ici pour la même raison, parce que je voulais implémenter Serializable alors j'ai essayé toutes les façons intuitives (pour moi) de le faire, et aucune d'entre elles ne fonctionne. Croyez-moi, la dernière chose que je chercherais est l'explication pour laquelle je n'ai pas besoin de cette fonctionnalité et que je devrais opter pour autre chose. Un an et demi, Jésus-Christ ! Je parie qu'il y a déjà un PR quelque part, avec des tests et des trucs. S'il vous plaît, faites en sorte que cela se produise, et s'il y a un certain mode d'utilisation qui est découragé, nous avons un tslint pour cela.

Il est possible d'accéder aux membres statiques des classes enfants à partir de la classe de base via this.constructor.staticMember , donc les membres statiques abstraits ont du sens pour moi.

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

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

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

La classe A devrait pouvoir spécifier qu'elle nécessite un membre statique x , car elle l'utilise dans la méthode f .

Des nouvelles ?
Les fonctionnalités sont vraiment nécessaires 😄
1) Fonctions static dans les interfaces
2) Fonctions abstract static dans les classes abstraites

bien que je déteste l'idée d'avoir des interfaces statiques, mais à toutes fins pratiques, ce qui suit devrait suffire aujourd'hui :

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

qui peut être utilisé comme :

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

METTRE À JOUR:

plus de détails sur l'idée et un exemple en direct :

// 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, ce n'est peut-être pas la faute de Typescript, mais je n'ai pas pu obtenir ces décorateurs de composants angulaires fonctionnels et leur compilateur AoT.

@aleksey-bykov c'est intelligent mais ça ne marche toujours pas pour la statique abstraite. Si vous avez des sous-classes de MyClass , elles ne sont pas appliquées avec la vérification de type. C'est aussi pire si vous avez des génériques impliqués.

// no errors
class Thing extends MyClass {

}

J'espère vraiment que l'équipe TypeScript reconsidérera sa position à ce sujet, car la création de bibliothèques d'utilisateurs finaux nécessitant des attributs statiques n'a aucune implémentation raisonnable. Nous devrions pouvoir avoir un contrat qui exige que les implémenteurs d'interface/extendeurs de classe abstraite aient des statiques.

@bbugh je remets en question l'existence même du problème discuté ici, pourquoi auriez-vous besoin de tous ces problèmes avec des méthodes héritées abstraites statiques si la même chose peut être faite via des instances de classes régulières?

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 Il y a plein de raisons. Par exemple ( de @patryk-zielinski93):

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

Je veux forcer l'implémentation de la méthode de désérialisation statique dans les sous-classes de Serializable.
Existe-t-il une solution de contournement pour mettre en œuvre un tel comportement?

EDIT : Je sais que vous pouvez également utiliser deserialize comme constructeur, mais les classes ne peuvent avoir qu'un seul constructeur, ce qui rend les méthodes d'usine nécessaires. Les gens veulent un moyen d'exiger des méthodes d'usine dans les interfaces, ce qui est tout à fait logique.

prenez simplement la logique de désérialisation dans une classe distincte, car il n'y a aucun avantage à avoir une méthode de désérialisation statique attachée à la classe même désérialisée

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

quel est le problème?

@aleksey-bykov la classe séparée n'a pas l'air aussi jolie

@aleksey-bykov, le problème est qu'il y a plus d'une classe nécessitant une sérialisation, donc votre approche oblige à créer un dictionnaire de sérialisables, qui est un antipattern uberclass, et chaque modification de l'un d'entre eux nécessiterait une modification dans cette uberclass, ce qui rend le support du code une nuisance. Bien qu'une interface sérialisable puisse forcer une implémentation à n'importe quel type d'objet donné, et également prendre en charge l'héritage polymorphe, qui sont les principales raisons pour lesquelles les gens le souhaitent. S'il vous plaît, ne spéculez pas s'il y a un avantage ou non, n'importe qui devrait pouvoir avoir des options et choisir ce qui est bénéfique pour son propre projet.

@octaharon avoir une classe uniquement dédiée à la désérialisation est tout à fait le contraire de ce que vous avez dit, elle a une responsabilité unique car la seule fois où vous la modifiez, c'est lorsque vous vous souciez de la désérialisation

contraire, ajouter la désérialisation à la classe elle-même comme vous l'avez proposé lui donne une responsabilité supplémentaire et une raison supplémentaire d'être modifiée

enfin, je n'ai aucun problème avec les méthodes statiques, mais je suis vraiment curieux de voir un exemple de cas d'utilisation pratique pour les méthodes statiques abstraites et les interfaces statiques

@octaharon avoir une classe uniquement dédiée à la désérialisation est tout à fait le contraire de ce que vous avez dit, elle a une responsabilité unique car la seule fois où vous la modifiez, c'est lorsque vous vous souciez de la désérialisation

sauf que vous devez modifier le code à deux endroits au lieu d'un, car votre (dé)sérialisation dépend de votre structure de type. Cela ne compte pas pour la "responsabilité unique"

contraire, ajouter la désérialisation à la classe elle-même comme vous l'avez proposé lui donne une responsabilité supplémentaire et une raison supplémentaire d'être modifiée

Je ne comprends pas ce que vous dites. Une classe qui est responsable de sa propre (dé)sérialisation est exactement ce que je veux, et s'il vous plaît, ne me dites pas si c'est bien ou mal.

la responsabilité unique ne dit rien sur le nombre de fichiers impliqués, elle indique seulement qu'il doit y avoir une seule raison

ce que je dis, c'est que lorsque vous ajoutez une nouvelle classe, vous l'entendez pour autre chose que la simple désérialisation, elle a donc déjà une raison d'exister et une responsabilité qui lui est attribuée; puis vous ajoutez une autre responsabilité de pouvoir se désérialiser, cela nous donne 2 responsabilités, cela viole SOLID, si vous continuez à y ajouter plus de choses comme le rendu, l'impression, la copie, le transfert, le chiffrement, etc., vous obtiendrez une classe de dieu

et pardonnez-moi de vous dire des banalités, mais les questions (et les réponses) sur les avantages et les cas d'utilisation pratiques sont ce qui motive la mise en œuvre de cette proposition de fonctionnalité

SOLID n'a pas été envoyé par Dieu Tout-Puissant sur une tablette, et même si c'était le cas, il y a un degré de liberté dans son interprétation. Vous pourriez être aussi idéaliste que vous le souhaitez à ce sujet, mais faites-moi une faveur : ne répandez pas vos croyances dans la communauté. Tout le monde a tous les droits d'utiliser n'importe quel outil de la manière qu'il veut et d'enfreindre toutes les règles possibles qu'il connaît (vous ne pouvez pas blâmer un couteau pour un meurtre). Ce qui définit la qualité d'un outil est un équilibre entre une demande pour certaines fonctionnalités et une offre pour celles-ci. Et cette branche montre le volume de la demande. Si vous n'avez pas besoin de cette fonctionnalité, ne l'utilisez pas. Je fais. Et un tas de gens ici en ont aussi besoin, et _nous_ avons un cas d'utilisation pratique, alors que vous dites que nous devrions l'ignorer, en fait. Il ne s'agit que de ce qui est le plus important pour les mainteneurs - les principes sacrés (de quelque manière qu'ils le comprennent) ou la communauté.

mec, toute bonne proposition comporte des cas d'utilisation, celle-ci ne le fait pas

image

donc j'ai seulement exprimé une certaine curiosité et posé une question, puisque la proposition ne le dit pas, pourquoi vous pourriez en avoir besoin

cela se résumait à typique : juste cause, n'ose pas

personnellement, je me fiche de solid ou oop, je l'ai juste envahi il y a longtemps, vous l'avez soulevé en lançant l'argument "uberclass antipattern" puis en sauvegardant "un degré de liberté dans son interprétation"

la seule raison pratique mentionnée dans toute cette discussion est la suivante : https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119

Un cas d'utilisation très courant avec lequel cela vous aidera est les composants React, qui sont des classes avec des propriétés statiques telles que displayName, propTypes et defaultProps.

et quelques articles similaires https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -345496014

mais c'est couvert par (new (...) => MyClass) & MyStaticInterface

c'est le cas d'utilisation exact et une raison pour laquelle je suis ici. Vous voyez le nombre de votes ? Pourquoi pensez-vous que c'est à vous personnellement de décider ce qui est practical et ce qui ne l'est pas ? Practical est ce qui peut être mis dans practice , et (au moment de l'écriture) 83 personnes trouveraient cette fonctionnalité sacrément pratique. Veuillez respecter les autres et lire le fil de discussion complet avant de commencer à sortir les phrases d'un contexte et à exposer divers mots à la mode. Quoi que vous ayez surmonté, ce n'est certainement pas votre ego.

c'est du bon sens que les choses pratiques sont celles qui résolvent les problèmes, les choses non pratiques sont celles qui stimulent votre sens de la beauté, je respecte les autres mais avec tout ce respect, la question (principalement rhétorique maintenant) tient toujours : quel problème cette proposition a-t-elle l'intention de résoudre résoudre (new (...) => MyClass) & MyStaticInterface pour https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119 et https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -289084844 juste cause

s'il vous plaît ne répondez pas

Pour la même raison, j'utilise parfois des déclarations type pour réduire les annotations volumineuses, je pense qu'un mot-clé comme abstract static serait beaucoup plus lisible qu'une construction relativement plus difficile à digérer comme celle qui a été mentionnée comme existante Exemple.

De plus, nous n'avons toujours pas abordé les classes abstraites ?

La solution pour ne pas utiliser de classes abstraites n'est pas la solution, à mon avis. C'est une solution de contournement ! Une solution autour de quoi ?

Je pense que cette demande de fonctionnalité existe parce que de nombreuses personnes, y compris le demandeur, ont constaté qu'une fonctionnalité attendue telle que abstract static ou static dans les interfaces n'était pas présente.

Sur la base de la solution proposée, le mot-clé static doit-il même exister s'il existe une solution de contournement pour éviter son utilisation ? Je pense que ce serait tout aussi ridicule de suggérer.

Le problème ici est que static a beaucoup plus de sens.
Sur la base de l'intérêt généré, pouvons-nous avoir une discussion moins dédaigneuse ?

Y a-t-il eu une mise à jour sur cette proposition? Y a-t-il des arguments à considérer qui démontrent pourquoi nous ne devrions pas avoir static abstract et autres ?
Pouvons-nous avoir d'autres suggestions qui montrent pourquoi ce serait utile?

Peut-être devrions-nous résumer les choses et résumer ce qui a été discuté afin que nous puissions trouver une solution.

Il y a deux propositions si je comprends bien :

  1. les interfaces et les types peuvent définir des propriétés et des méthodes statiques
interface ISerializable<T> { 
   static fromJson(json: string): T;
}
  1. les classes abstraites peuvent définir des méthodes statiques abstraites
abstract class MyClass<T> implements ISerializable<T> {
   abstract static fromJson(json: string): T;
}

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

Quant à la première proposition, il existe techniquement une solution de contournement ! Ce qui n'est pas génial, mais c'est quelque chose au moins. Mais c'est une solution de contournement.

Vous pouvez diviser vos interfaces en deux et réécrire votre classe comme

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

interface InstanceInterface {
  toJson(): string;
}

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

À mon avis, c'est beaucoup de travail supplémentaire et légèrement moins lisible, et a l'inconvénient de réécrire vos classes d'une manière amusante, ce qui est tout simplement étrange et s'écarte de la syntaxe que nous utilisons.

Mais alors, qu'en est-il de la proposition 2 ? Il n'y a rien à faire à ce sujet, n'est-ce pas? Je pense que ça mérite aussi d'être abordé !

Quelle est l'utilisation pratique de l'un de ces types ? Comment l'un d'entre eux serait-il utilisé ?

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

Il est déjà possible de dire qu'une valeur doit être un objet comme { fromJSON(serializedValue: string): JsonSerializable; } , donc est-ce juste voulu pour appliquer un modèle ? Je ne vois pas l'avantage à cela du point de vue de la vérification de type. En remarque : dans ce cas, il s'agirait d'appliquer un modèle avec lequel il est difficile de travailler. Il serait préférable de déplacer le processus de sérialisation dans des classes ou des fonctions de sérialisation distinctes pour de nombreuses raisons que je n'aborderai pas ici.

De plus, pourquoi quelque chose comme ça est-il fait?

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

Qu'en est-il de l'utilisation de la méthode de modèle ou du modèle de stratégie à la place ? Cela fonctionnerait et serait plus flexible, non ?

Pour le moment, je suis contre cette fonctionnalité car elle me semble ajouter une complexité inutile pour décrire une conception de classe avec laquelle il est difficile de travailler. Peut-être y a-t-il un avantage qui me manque?

il existe un cas d'utilisation valide pour les méthodes React statiques, c'est à peu près tout

@aleksey-bykov ah, d'accord. Dans ces cas, il serait peut-être préférable que l'ajout d'un type sur la propriété constructor entraîne la vérification du type dans ce scénario rare. Par exemple:

interface Component {
    constructor: ComponentConstructor;
}

interface ComponentConstructor {
    displayName?: string;
}

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

Cela me semble bien plus intéressant. Cela n'ajoute aucune complexité supplémentaire au langage et ajoute seulement plus de travail pour le vérificateur de type lors de la vérification si une classe implémente correctement une interface.


Au fait, je pensais qu'un cas d'utilisation valide serait lors du déplacement d'une instance vers le constructeur et enfin vers une méthode ou une propriété statique avec vérification de type, mais c'est déjà possible en tapant la propriété constructor sur un type et sera résolu pour les instances de classe dans #3841.

Le modèle de sérialisation/désérialisation décrit en amont n'est-il pas "valide" ?

@dsherret ne "voit pas l'avantage du point de vue de la vérification de type". L'intérêt de faire une analyse statique en premier lieu est de détecter les erreurs le plus tôt possible. Je tape des choses de sorte que si une signature d'appel doit changer, alors tous ceux qui l'appellent - ou, surtout, tous ceux qui sont responsables de la mise en œuvre des méthodes qui utilisent la signature - mettent à jour la nouvelle signature.

Supposons que j'ai une bibliothèque qui fournit un ensemble de classes sœurs avec une méthode statique foo(x: number, y: boolean, z: string) , et que l'on s'attend à ce que les utilisateurs écrivent une classe d'usine qui prend plusieurs de ces classes et appelle la méthode pour construire des instances. (Peut-être que c'est deserialize ou clone ou unpack ou loadFromServer , peu importe.) L'utilisateur crée également des sous-classes du même parent (éventuellement abstrait) classe de la bibliothèque.

Maintenant, je dois modifier ce contrat afin que le dernier paramètre soit un objet d'options. Passer une valeur de chaîne pour le 3ème argument est une erreur irrécupérable qui doit être signalée au moment de la compilation. Toute classe de fabrique doit être mise à jour pour transmettre un objet à la place lors de l'appel foo , et les sous-classes qui implémentent foo doivent changer leur signature d'appel.

Je veux garantir que les utilisateurs qui mettent à jour vers la nouvelle version de la bibliothèque capteront les changements avec rupture au moment de la compilation. La bibliothèque doit exporter l'une des solutions de contournement d'interface statique ci-dessus (comme type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface; ) et espérer que le consommateur l'a appliquée aux bons endroits. S'ils ont oublié de décorer l'une de leurs implémentations ou s'ils n'ont pas utilisé un type exporté de bibliothèque pour décrire la signature d'appel de foo dans leur classe d'usine, le compilateur ne peut rien dire de changé et ils obtiennent des erreurs d'exécution . Comparez cela avec une implémentation sensée des méthodes abstract static dans la classe parent - aucune annotation spéciale requise, aucune charge pour le code consommateur, cela fonctionne tout simplement.

@thw0rted la plupart des bibliothèques ne nécessiteront-elles pas une sorte d'enregistrement de ces classes et la vérification de type peut-elle avoir lieu à ce stade? Par exemple:

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

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

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

Ou est-ce que des instances de ces classes sont utilisées avec la bibliothèque et la bibliothèque passe de l'instance au constructeur afin d'obtenir les méthodes statiques ? Si tel est le cas, il est possible de modifier la conception de la bibliothèque pour ne pas nécessiter de méthodes statiques.

En passant, en tant qu'implémenteur d'une interface, je serais très ennuyé si j'étais obligé d'écrire des méthodes statiques car il est très difficile d'injecter des dépendances à une méthode statique en raison de l'absence de constructeur (aucune dépendance ctor n'est possible). Cela rend également difficile le remplacement de l'implémentation de la désérialisation par autre chose en fonction de la situation. Par exemple, disons que je désérialisais à partir de différentes sources avec différents mécanismes de sérialisation... j'ai maintenant une méthode de désérialisation statique qui, par conception, ne peut être implémentée que dans un sens pour une implémentation d'une classe. Pour contourner ce problème, j'aurais besoin d'une autre propriété ou méthode statique globale sur la classe pour indiquer à la méthode de désérialisation statique quoi utiliser. Je préférerais une interface Serializer séparée qui permettrait d'échanger facilement ce qu'il faut utiliser et ne couplerait pas la (dé)sérialisation à la classe en cours de (dé)sérialisation.

Je vois ce que vous voulez dire - pour utiliser un modèle d'usine, vous devez éventuellement transmettre la classe d'implémentation à l'usine, et la vérification statique se produit à ce moment-là. Je pense toujours qu'il peut arriver que vous souhaitiez fournir une classe conforme à l'usine mais que vous ne l'utilisiez pas réellement pour le moment, auquel cas une contrainte abstract static détecterait les problèmes plus tôt et clarifierait le sens.

J'ai un autre exemple qui, je pense, ne va pas à l'encontre de votre préoccupation "sidenote". J'ai un certain nombre de classes sœurs dans un projet frontal, où je donne à l'utilisateur le choix du "fournisseur" à utiliser pour une certaine fonctionnalité. Bien sûr, je pourrais créer un dictionnaire quelque part de name => Provider et utiliser les touches pour déterminer ce qu'il faut afficher dans la liste de sélection, mais la façon dont je l'ai implémenté maintenant est d'exiger un champ statique name sur chaque implémentation de fournisseur.

À un moment donné, j'ai changé cela pour exiger à la fois un shortName et longName (pour un affichage dans différents contextes avec différentes quantités d'espace d'écran disponible). Il aurait été beaucoup plus simple de changer abstract static name: string; en abstract static shortName: string; etc, au lieu de changer le composant de la liste de sélection pour avoir un providerList: Type<Provider> & { shortName: string } & { longName: string } . Il transmet l'intention, au bon endroit (c'est-à-dire dans le parent abstrait, pas dans le composant consommateur), et est facile à lire et facile à modifier. Je pense que nous pouvons dire qu'il existe une solution de contournement, mais je pense toujours que c'est objectivement pire que les changements proposés.

Je suis récemment tombé sur ce problème, lorsque j'avais besoin d'utiliser une méthode statique dans une interface. Je m'excuse si cela a déjà été abordé auparavant, car je n'ai pas le temps de lire cette quantité de commentaires.

Mon problème actuel : j'ai une bibliothèque privée .js, que je souhaite utiliser dans mon projet TypeScript. Je suis donc allé de l'avant et j'ai commencé à écrire un fichier .d.ts pour cette bibliothèque, mais comme la bibliothèque utilise des méthodes statiques, je n'ai pas vraiment pu terminer cela. Quelle est l'approche suggérée dans ce cas?

Merci pour les réponses.

@greeny voici une solution de travail : https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

spécifiquement cette partie de celui-ci:

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

Autre cas d'utilisation : code généré / shims de classe partielle

  • J'utilise la bibliothèque @rsuter/nswag, qui génère des spécifications swagger.
  • Vous pouvez écrire un fichier 'extensions' qui sera fusionné dans le fichier généré.
  • Le fichier d'extensions n'est jamais exécuté, mais il doit se compiler tout seul !
  • Parfois, dans ce fichier, j'ai besoin de faire référence à quelque chose qui n'existe pas encore (parce qu'il est généré)
  • Par conséquent, je souhaite déclarer un shim / interface pour celui-ci comme suit
  • Remarque : vous pouvez spécifier que certaines instructions import sont ignorées lors de la fusion finale afin que l'inclusion de 'shim' soit ignorée dans le code généré final.
interface SwaggerException
{
    static isSwaggerException(obj: any): obj is SwaggerException;
}

Mais je ne peux pas faire cela et je dois en fait écrire une classe - ce qui est bien mais me semble toujours faux. Je veux juste - comme beaucoup d'autres l'ont dit - dire "c'est le contrat"

Je voulais juste ajouter ceci ici car je ne vois aucune autre mention de génération de code - mais beaucoup de commentaires "pourquoi auriez-vous besoin de cela" qui deviennent tout simplement irritants. Je m'attendrais à ce qu'un pourcentage décent de personnes "ayant besoin" de cette fonctionnalité "statique" dans une interface le fasse simplement pour pouvoir se référer à des éléments de bibliothèque externes.

De plus, j'aimerais vraiment voir des fichiers d.ts plus propres - il doit y avoir un chevauchement avec cette fonctionnalité et ces fichiers. Il est difficile de comprendre quelque chose comme JQueryStatic car il semble que ce ne soit qu'un hack. De plus, la réalité est que les fichiers d.ts sont souvent obsolètes et non entretenus et vous devez déclarer vous-même les cales.

(désolé de mentionner jQuery)

Pour le cas de sérialisation, j'ai fait quelque chose comme ça.

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

Mais ce serait encore beaucoup plus facile d'utiliser quelque chose comme ça:

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

J'ai lu beaucoup de ce fil et je ne comprends toujours pas pourquoi il est bon et correct de dire non à ce modèle. Si vous partagez cette opinion, vous dites également que java et d'autres langages populaires font fausse route. Ou ai-je tort?

Ce numéro est ouvert depuis deux ans et compte 79 commentaires. Il est étiqueté comme Awaiting More Feedback . Pourriez-vous dire de quels autres commentaires vous avez besoin pour prendre une décision ?

c'est vraiment ennuyeux que je ne puisse pas décrire la méthode statique ni dans l'interface ni dans la classe abstraite (déclaration uniquement). C'est tellement moche d'écrire des solutions de contournement car cette fonctionnalité n'a pas encore été implémentée =(

Par exemple, next.js utilise une fonction statique getInitialProps pour obtenir les propriétés de la page avant de construire la page. En cas de lancement, la page n'est pas construite, mais plutôt la page d'erreur.

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

Mais malheureusement, les indices ts qui implémentent cette méthode peuvent lui donner n'importe quelle signature de type, même si cela provoque des erreurs au moment de l'exécution, car il ne peut pas être vérifié.

Je pense que ce problème existe ici depuis si longtemps parce que JavaScript lui-même n'est pas bon pour les choses statiques 🤔

L'héritage statique ne devrait jamais exister en premier lieu. 🤔🤔

Qui veut appeler une méthode statique ou lire un champ statique ? 🤔🤔🤔

  • classe enfant : elle ne doit pas être statique
  • classe parent : utiliser l'argument du constructeur
  • autre : utiliser l'interface IsomeClassConstructor

Y a-t-il un autre cas d'utilisation ?🤔🤔🤔🤔

Toute mise à jour à ce sujet?

Si cela peut vous aider, ce que j'ai fait dans les cas où j'ai besoin de taper l'interface statique pour une classe, c'est d'utiliser un décorateur pour appliquer les membres statiques sur la classe

Le décorateur est défini comme :

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

Si j'ai une interface de membre de constructeur statique définie comme

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

et invoqué sur la classe qui doit déclarer statiquement les membres sur T comme :

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

L'exemple le plus fréquent est le bon :

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

Mais comme dit dans #13462 ici :

Les interfaces doivent définir la fonctionnalité fournie par un objet. Cette fonctionnalité doit être remplaçable et interchangeable (c'est pourquoi les méthodes d'interface sont virtuelles). La statique est un concept parallèle au comportement dynamique/aux méthodes virtuelles.

Je suis d'accord sur le fait que les interfaces, dans TypeScript, décrivent simplement une instance d'objet elle-même et comment l'utiliser. Le problème est qu'une instance d' objet n'est pas une définition de classe , et un symbole static peut n'exister que sur une définition de classe .

Je peux donc proposer ce qui suit, avec tous ses défauts :


Une interface peut décrire soit un objet , soit une classe . Disons qu'une interface de classe est notée avec les mots clés class_interface .

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

Les classes (et les interfaces de classe) peuvent utiliser les mots-clés statically implements pour déclarer leurs symboles statiques à l'aide d'une interface d' objet (les interfaces de classe ne peuvent pas être implémentées statically ).

Les classes (et les interfaces de classe) utiliseraient toujours le mot-clé implements avec une interface d' objet ou une interface de classe .

Une interface de classe pourrait alors être le mélange entre une interface objet implémentée statiquement et une interface implémentée par instance. Ainsi, nous pourrions obtenir ce qui suit :

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

class_interface ISerDes implements ISerializable statically implements IDeserializable {}

De cette façon, les interfaces pourraient garder leur sens, et class_interface serait un nouveau type de symbole d'abstraction dédié aux définitions de classes.

Un petit cas d'utilisation non critique de plus :
Dans Angular pour la compilation AOT, vous ne pouvez pas appeler de fonctions dans les décorateurs (comme dans le décorateur de module @NgModule )
problème angulaire

Pour le module service worker, vous avez besoin de quelque chose comme ceci :
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
Notre environnement utilise des sous-classes, étendant la classe abstraite avec des valeurs par défaut et des propriétés abstraites à implémenter. Exemple de mise en œuvre similaire
Donc AOT ne fonctionne pas car la construction d'une classe est une fonction et génère une erreur du type : Function calls are not supported in decorators but ..

Pour le faire fonctionner et conserver la prise en charge de l'auto-complétion/du compilateur, il est possible de définir les mêmes propriétés au niveau statique. Mais pour "implémenter" des propriétés, nous avons besoin d'une interface avec des membres statiques ou des membres statiques abstraits dans une classe abstraite. Les deux pas encore possibles.

avec l'interface pourrait fonctionner de cette façon:

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

avec des statiques abstraites pourrait fonctionner de cette façon :

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

il existe des solutions de contournement , mais elles sont toutes faibles :/

@DanielRosenwasser @RyanCavanaugh s'excuse pour les mentions, mais il semble que cette suggestion de fonctionnalité - qui a beaucoup de soutien de la communauté, et je pense qu'elle serait assez facile à mettre en œuvre - a été enfouie profondément dans la catégorie Problèmes. L'un de vous a-t-il des commentaires sur cette fonctionnalité, et un PR serait-il le bienvenu ?

Ce problème n'est-il pas un doublon de #1263 ? 😛

26398 (Type check static members based on implements type's constructor property) ressemble à une meilleure solution... si quelque chose comme ça devait être implémenté, j'espère que c'est celui-là. Cela ne nécessite aucune syntaxe/analyse supplémentaire et n'est qu'un changement de vérification de type pour un seul scénario. Cela ne soulève pas non plus autant de questions que cela.

J'ai l'impression que les méthodes statiques dans les interfaces ne sont pas aussi intuitives que d'avoir des méthodes abstraites statiques sur des classes abstraites.

Je pense qu'il est un peu sommaire d'ajouter des méthodes statiques aux interfaces car une interface doit définir un objet, pas une classe. D'autre part, une classe abstraite devrait certainement être autorisée à avoir des méthodes abstraites statiques, puisque les classes abstraites sont utilisées pour définir des sous-classes. En ce qui concerne l'implémentation de ceci, il suffirait simplement de vérifier le type lors de l'extension de la classe abstraite (par exemple class extends MyAbstractClass ), pas lors de son utilisation comme type (par exemple let myInstance: MyAbstractClass ).

Exemple:

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

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

en ce moment par nécessité j'utilise ça

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

ce n'est pas pratique !

Je suis venu avec un problème où j'ajoute des propriétés à "l'objet", voici un exemple de bac à sable

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

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

Toute instance d'objet est désormais considérée comme ayant la propriété getInstanceId alors que seule la Object devrait le faire. Avec une propriété statique, le problème aurait été résolu.

Vous voulez augmenter ObjectConstructor, pas Object. Vous déclarez une méthode d'instance, alors que vous voulez réellement attacher une méthode au constructeur lui-même. Je pense que cela est déjà possible grâce à la fusion de déclarations:

````ts
déclarer globale {
interface ObjectConstructor {
bonjour() : chaîne ;
}
}

Objet.hello();
````

@thw0rted Excellent ! merci je ne connaissais pas ObjectConstructor

Le point le plus important est que vous augmentez le type de constructeur plutôt que le type d'instance. Je viens de rechercher la déclaration Object dans lib.es5.d.ts et j'ai trouvé qu'elle était de type ObjectConstructor .

J'ai du mal à croire que ce problème est toujours d'actualité. Il s'agit d'une fonctionnalité légitimement utile, avec plusieurs cas d'utilisation réels.

L'intérêt de TypeScript est de pouvoir garantir la sécurité des types dans notre base de code, alors pourquoi cette fonctionnalité est-elle toujours "en attente de commentaires" après deux ans de commentaires ?

Je suis peut-être loin de cela, mais quelque chose comme les métaclasses de Python serait-il capable de résoudre ce problème de manière native et sanctionnée (c'est-à-dire pas un hack ou une solution de contournement) sans violer le paradigme de TypeScript (c'est-à-dire en gardant des types TypeScript séparés pour l'instance et la classe)?

Quelque chose comme ça:

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

Honnêtement, la partie la plus délicate de cette approche semble être de trouver un mot-clé TypeScript-y significatif pour metaclass ... staticimplements , classimplements , withstatic , implementsstatic ... pas sûr.

C'est un peu comme la proposition de @GerkinDev mais sans le type d'interface séparé. Ici, il y a un seul concept d'interface, et ils peuvent être utilisés pour décrire la forme d'une instance ou d'une classe. Les mots clés dans la définition de la classe d'implémentation indiqueraient au compilateur de quel côté chaque interface doit être vérifiée.

Reprenons la discussion aux #34516 et #33892 selon la fonctionnalité que vous recherchez

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

Roam-Cooper picture Roam-Cooper  ·  3Commentaires

kyasbal-1994 picture kyasbal-1994  ·  3Commentaires

dlaberge picture dlaberge  ·  3Commentaires

remojansen picture remojansen  ·  3Commentaires

siddjain picture siddjain  ·  3Commentaires