Typescript: "this" polymorphe pour les membres statiques

Créé le 1 déc. 2015  ·  150Commentaires  ·  Source: microsoft/TypeScript

Lorsque nous essayons d'implémenter un système de modèle de style d'enregistrement actif assez basique, mais polymorphe, nous rencontrons des problèmes avec le système de type ne respectant pas this lorsqu'il est utilisé conjointement avec un constructeur ou un modèle/générique.

J'ai déjà posté à ce sujet ici, # 5493, et # 5492 semble également mentionner ce comportement.

Et voici un post SO que j'ai fait:
http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved

J'ai recyclé mon exemple de # 5493 dans ce ticket pour une discussion plus approfondie. Je voulais un ticket ouvert représentant le désir d'une telle chose et d'une discussion mais les deux autres sont fermés.

Voici un exemple qui décrit un modèle Factory qui produit des modèles. Si vous souhaitez personnaliser le BaseModel qui revient du Factory , vous devriez pouvoir le remplacer. Cependant, cela échoue car this ne peut pas être utilisé dans un membre statique.

// Typically in a library
export interface InstanceConstructor<T extends BaseModel> {
    new(fac: Factory<T>): T;
}

export class Factory<T extends BaseModel> {
    constructor(private cls: InstanceConstructor<T>) {}

    get() {
        return new this.cls(this);
    }
}

export class BaseModel {
    // NOTE: Does not work....
    constructor(private fac: Factory<this>) {}

    refresh() {
        // get returns a new instance, but it should be of
        // type Model, not BaseModel.
        return this.fac.get();
    }
}

// Application Code
export class Model extends BaseModel {
    do() {
        return true;
    }
}

// Kinda sucks that Factory cannot infer the "Model" type
let f = new Factory<Model>(Model);
let a = f.get();

// b is inferred as any here.
let b = a.refresh();

Peut-être que ce problème est stupide et qu'il existe une solution de contournement facile. J'accueille les commentaires sur la façon dont un tel modèle peut être réalisé.

In Discussion Suggestion

Commentaire le plus utile

Y a-t-il une raison pour laquelle ce problème a été fermé ?

Le fait que cela polymorphe ne fonctionne pas sur la statique rend essentiellement cette fonctionnalité DOA, à mon avis. À ce jour, je n'ai jamais vraiment eu besoin de ceci polymorphe sur les membres d'instance, mais j'ai eu besoin toutes les quelques semaines de statique, puisque le système de gestion de la statique a été finalisé il y a bien longtemps. J'étais fou de joie lorsque cette fonctionnalité a été annoncée, puis déçue lorsque j'ai réalisé qu'elle ne fonctionnait que sur les membres de l'instance.

Le cas d'utilisation est très basique et extrêmement courant. Considérez une méthode d'usine simple :

class Animal
{
    static create(): this
    {
        return new this();
    }
}

class Bunny extends Animal
{
    hop()
    {
    }
}

Bunny.create().hop() // Type error!! Come on!!

À ce stade, j'ai eu recours à des méthodes de casting moche ou de litière static create() dans chaque héritier. Ne pas avoir cette fonctionnalité semble être un trou d'exhaustivité assez important dans la langue.

Tous les 150 commentaires

La taille et la forme de mon bateau sont assez similaires ! Ohé !

Une méthode de fabrique dans une superclasse renvoie de nouvelles instances de ses sous-classes. La fonctionnalité de mon code fonctionne mais m'oblige à caster le type de retour :

class Parent {
    public static deserialize(data: Object): any { ... create new instance ... }
    // Can't return a this type from statics! ^^^ :(
}

class Child extends Parent { ... }

let data = { ... };
let aChild: Child = Child.deserialize(data);
//           ^^^ Requires a cast as type cannot be inferred.

J'ai aussi rencontré ce problème aujourd'hui !

Une solution de correction consiste à transmettre le type enfant en tant que générique étendant la classe de base, ce qui est une solution que j'applique pour le moment :

class Parent {
    static create<T extends Parent>(): T {
        let t = new this();

        return <T>t;
    }
}

class Child extends Parent {
    field: string;
}

let b = Child.create<Child>();

Y a-t-il une raison pour laquelle ce problème a été fermé ?

Le fait que cela polymorphe ne fonctionne pas sur la statique rend essentiellement cette fonctionnalité DOA, à mon avis. À ce jour, je n'ai jamais vraiment eu besoin de ceci polymorphe sur les membres d'instance, mais j'ai eu besoin toutes les quelques semaines de statique, puisque le système de gestion de la statique a été finalisé il y a bien longtemps. J'étais fou de joie lorsque cette fonctionnalité a été annoncée, puis déçue lorsque j'ai réalisé qu'elle ne fonctionnait que sur les membres de l'instance.

Le cas d'utilisation est très basique et extrêmement courant. Considérez une méthode d'usine simple :

class Animal
{
    static create(): this
    {
        return new this();
    }
}

class Bunny extends Animal
{
    hop()
    {
    }
}

Bunny.create().hop() // Type error!! Come on!!

À ce stade, j'ai eu recours à des méthodes de casting moche ou de litière static create() dans chaque héritier. Ne pas avoir cette fonctionnalité semble être un trou d'exhaustivité assez important dans la langue.

@paul-go le sujet n'est pas clos... ?

@paul-go J'ai également été frustré par ce problème, mais la solution ci-dessous est la solution de contournement la plus fiable que j'ai trouvée. Chaque sous-classe Animal devrait appeler super.create() et simplement convertir le résultat en son type. Ce n'est pas grave et c'est une doublure qui peut facilement être retirée avec cela.

Le compilateur, intellisense et surtout le lapin sont tous satisfaits.

class Animal {
    public static create<T extends Animal>(): T {
        let TClass = this.constructor.prototype;
        return <T>( new TClass() );
    }
}

class Bunny extends Animal {    
    public static create(): Bunny {
        return <Bunny>super.create();
    }

    public hop(): void {
        console.log(" Hoppp!! :) ");
    }
}

Bunny.create().hop();

         \\
          \\_ " See? I am now a happy Bunny! "
           (')   " Don't be so hostile! "
          / )=           " :P "
        o( )_


@RyanCavanaugh Oups ... pour une raison quelconque, j'ai confondu cela avec # 5862 ... désolé pour l'agression de la hache de combat :-)

@ Think7 Yep ... d'où le "recours à des méthodes laids de coulée ou de litière statique create () dans chaque héritier". C'est assez difficile cependant lorsque vous êtes un développeur de bibliothèque et que vous ne pouvez pas vraiment forcer les utilisateurs finaux à implémenter un tas de méthodes statiques typées dans les classes dont ils héritent de vous.

loi. Totalement raté tout sous votre code :D

Ça valait le coup, je dois dessiner un lapin.

:+1: lapin

:lapin: :coeur:

+1, j'aimerais vraiment voir ça

Y a-t-il eu des mises à jour de discussion sur ce sujet ?

Il reste sur notre énorme arriéré de suggestions.

Javascript agit déjà correctement dans un tel modèle. Si TS pouvait également suivre, cela nous éviterait beaucoup de code passe-partout/supplémentaire. Le "modèle de modèle" est assez standard, je m'attendrais à ce que TS fonctionne comme JS sur ce point.

J'aimerais aussi beaucoup cette fonctionnalité pour les mêmes raisons de "modèle CRUD" que tout le monde. J'en ai besoin sur les méthodes statiques plus que sur les méthodes d'instance.

Cela fournirait une solution soignée au problème décrit dans #8164.

C'est bien qu'il y ait des "solutions" avec des remplacements et des génériques, mais ils ne résolvent vraiment rien ici - le but de cette fonctionnalité est d'éviter de tels remplacements / conversions et de créer une cohérence avec le retour this type est géré dans les méthodes d'instance.

Je travaille sur les typages pour Sequelize 4.0 et il utilise une approche où vous sous-classez une classe Model . Cette classe Model a d'innombrables méthodes statiques comme findById() etc. qui bien sûr ne renvoient pas un Model mais votre sous-classe, alias this dans le contexte statique :

abstract class Model {
    public static tableName: string;
    public static findById(id: number): this { // error: a this type is only available in a non-static member of a class or interface 
        const rows = db.query(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
        const instance = new this();
        for (const column of Object.keys(rows[0])) {
            instance[column] = rows[0][column];
        }
        return instance;        
    }
}

class User extends Model {
    public static tableName = 'users';
    public username: string;    
}

const user = User.findById(1); // user instanceof User

Ce n'est pas possible de taper actuellement. Sequelize est _the_ ORM pour Node et il est dommage qu'il ne puisse pas être saisi. Vraiment besoin de cette fonctionnalité. Le seul moyen est de lancer chaque fois que vous appelez l'une de ces fonctions ou de remplacer chacune d'entre elles, d'adapter le type de retour et de ne rien faire d'autre que d'appeler super.method() .

Un autre type de relation est que les membres statiques ne peuvent pas référencer des arguments de type génériques - certaines des méthodes prennent un objet littéral d'attributs pour le modèle qui pourrait être typé via un argument de type, mais elles ne sont disponibles que pour les membres d'instance.

😰 Je ne peux pas croire que ce n'est toujours pas corrigé / ajouté .....

On pourrait en faire bon usage :

declare class NSObject {
    init(): this;
    static alloc(): this;
}

declare class UIButton extends NSObject {
}

let btn: UIButton = UIButton.alloc().init();

voici un cas d'utilisation que je souhaite travailler (migré de https://github.com/Microsoft/TypeScript/issues/9775 que j'ai fermé en faveur de cela)

Actuellement, les paramètres d'un constructeur ne peuvent pas utiliser le type this :

class C<T> {
    constructor(
        public transformParam: (self: this) => T // not works
    ){
    }
    public transformMethod(self: this) : T { // works
        return undefined;
    }
}

Prévu : disponible pour les paramètres du constructeur.

Un problème que l'on cherche à résoudre :

  • pouvoir baser mon API fluent soit autour d'elle-même soit autour d'une autre API fluent réutilisant le même code :
class TheirFluentApi {
    totallyUnrelated(): TheirFluentApi {
        return this;
    }
}

class MyFluentApi<FluentApi> {
    constructor(
        public toNextApi: (self: this) => FluentApi // let's imagine it works
    ){
    }
    one(): FluentApi {
        return this.toNextApi(this);
    }
    another(): FluentApi {
        return this.toNextApi(this);
    }
}

// self based fluent API;
const selfBased = new MyFluentApi(this => this);
selfBased.one().another();

// foreign based fluent API:
const foreignBased = new MyFluentApi(this => new TheirFluentApi());
foreignBased.one().totallyUnrelated();

Solution de contournement à l'épreuve du temps :

class Foo {

    foo() { }

    static create<T extends Foo>(): Foo & T {
        return new this() as Foo & T;
    }
}

class Bar extends Foo {

    bar() {}
}

class Baz extends Bar {

    baz() {}
}

Baz.create<Baz>().foo()
Baz.create<Baz>().bar()
Baz.create<Baz>().baz()

De cette façon, lorsque TypeScript prend en charge this pour les membres statiques, le nouveau code utilisateur sera Baz.create() au lieu de Baz.create<Baz>() tandis que l'ancien code utilisateur fonctionnera parfaitement. :le sourire:

C'est vraiment nécessaire! en particulier pour DAO qui ont des méthodes statiques renvoyant l'instance. La plupart d'entre eux sont définis sur une base DAO, comme (save, get, update etc...)

Je peux dire que cela pourrait causer une certaine confusion, avoir le type this sur une méthode statique résolue dans la classe dans laquelle elle se trouve et non le type de la classe (c'est-à-dire: typeof ).

Dans le vrai JS, appeler une méthode statique sur un type se traduira par this à l'intérieur de la fonction étant la classe et non une instance de celle-ci... donc c'est incohérent.

Cependant, dans l'intuition des gens, je pense que la première chose qui apparaît lorsque l'on voit le type de retour this sur une méthode statique est le type d'instance...

@shlomiassaf Ce ne serait pas incohérent. Lorsque vous spécifiez une classe comme type de retour pour une fonction telle que User, le type de retour sera une instance de l'utilisateur. Exactement la même chose, lorsque vous définissez un type de retour de this sur une méthode statique, le type de retour sera une instance de this (une instance de la classe). Une méthode qui renvoie la classe elle-même peut alors être modélisée avec typeof this.

@felixfbecker c'est totalement une chose de point de vue, c'est ainsi que vous choisissez de le regarder.

Inspectons ce qui se passe dans JS, afin que nous puissions en déduire la logique :

class Example {
  myFunc(): this {
    return this; 
  }

  static myFuncStatic(): this {
    return this;   // this === Example
  }
}

new Example().myFunc() //  instanceof Exapmle === true
Example.myFuncStatic() // === Example

Maintenant, this dans le temps d'exécution réel est le contexte délimité de la fonction, c'est exactement ce qui se passe dans les interfaces fluides comme les API et le polymorphisme de cette fonctionnalité aide en renvoyant le bon type, qui s'aligne simplement sur le fonctionnement de JS, Une classe de base renvoyant this renvoie l'instance créée par la classe dérivée. La fonctionnalité est en fait un correctif.

Pour résumer:
Une méthode renvoyant this qui est définie dans le cadre du prototype (instance) est censée renvoyer l'instance.

En continuant avec cette logique, une méthode renvoyant this qui est définie comme faisant partie du type de classe (prototype) devrait renvoyer le contexte délimité, qui est le type de classe.

Encore une fois, pas de parti pris, pas d'opinion, des faits simples.

Intuitivement, je me sentirai à l'aise d'avoir this renvoyé par une fonction statique représentant l'instance puisqu'elle est définie dans le type, mais c'est moi. D'autres pourraient penser différemment et nous ne pouvons pas leur dire qu'ils ont tort.

Le problème est qu'il doit être possible de taper à la fois une méthode statique qui renvoie une instance de classe et une méthode statique qui renvoie la classe elle-même (type de this). Votre logique a du sens du point de vue de JS, mais nous parlons ici de return _types_, et l'utilisation d'une classe comme type de retour (ici this) dans TypeScript et tout autre langage signifie toujours l'instance de la classe. Pour retourner la classe réelle, nous avons l'opérateur typeof.

@felixfbecker Cela soulève un autre problème !

Si le type this est le type d'instance, il est différent de ce à quoi le mot-clé this fait référence dans le corps de la méthode statique. return this donne un type de retour de fonction de typeof this , ce qui est totalement bizarre !

Non ce n'est pas. Lorsque vous définissez une méthode comme

getUser(): User {
  ...
}

vous vous attendez à récupérer un User _instance_, pas la classe User (c'est à cela que sert typeof ). C'est comme ça que ça marche dans toutes les langues typées. La sémantique de type est simplement différente de la sémantique d'exécution.

Pourquoi ne pas utiliser les mots-clés this ou static comme constructeur dans func pour manipuler avec une classe enfant ?

class Model {
  static find():this[] {
    return [new this("prop")]; // or new static(...)
  }
}

class Entity extends Model {
  constructor(public prop:string) {}
}

Entity.find().map(x => console.log(x.prop));

Et si on compare ça avec un exemple en JS, on verra ce que ça marche correctement :

class Model {
  static find() { 
    return [new this] 
  }
}

class Entity extends Model {
  constructor(prop) {
    this.prop = prop;
  }
}

Entity.find().map(x => console.log(x.prop))

Vous ne pouvez pas utiliser le type this sur une méthode statique, je pense que c'est toute la racine du problème.

@felixfbecker

Considère ceci:

class Greeter {
    static getHandle(): this {
        return this;
    }
}

Cette annotation de type est intuitive, mais incorrecte si le type this dans une méthode statique est l'instance de classe. Le mot-clé this a un type de typeof this dans une méthode statique !

Je pense que this _devrait_ faire référence au type d'instance, pas au type de classe, car nous pouvons obtenir une référence au type de classe à partir du type d'instance ( typeof X ) mais pas l'inverse ( instancetypeof X ?)

@xealot d' accord, pourquoi ne pas utiliser le mot-clé static la place this ? Mais this dans le contexte statique JS pointe toujours vers un constructeur.

@izatop oui, le javascript généré fonctionne (correctement ou incorrectement). Cependant, il ne s'agit pas de Javascript. La plainte n'est pas que le langage Javascript n'autorise pas ce modèle, c'est que le système de type de Typescript ne le permet pas.

Vous ne pouvez pas maintenir la sécurité des types avec ce modèle car Typescript n'autorise pas les types polymorphes this sur les membres statiques. Cela inclut les méthodes de classe définies avec le mot clé static et le constructor .

Lien de l'aire de jeux

@LPGhatguy mais vous avez lu mon commentaire précédent, n'est-ce pas ?! Si vous avez une méthode avec le type de retour User , vous attendez return new User(); , pas return User; . De la même manière, une valeur de retour de this devrait signifier return new this(); dans une méthode statique. Dans les méthodes non statiques, c'est différent, mais c'est clair parce que this est toujours un objet, vous ne pourrez pas l'instancier. Mais au final, nous sommes tous d'accord sur le fait qu'il s'agit de la meilleure syntaxe à cause de typeof et que cette fonctionnalité est indispensable dans TS.

@xealot je comprends ce que TypeScript n'autorise pas polymorphe this , cependant je vais vous demander pourquoi ne pas ajouter cette fonctionnalité à TS?

Je ne sais pas si ce qui suit fonctionnera pour tous les cas d'utilisation de ce problème, mais j'ai découvert que l'utilisation du type this: dans les méthodes statiques en conjonction avec une utilisation intelligente du système d'inférence de type permet de s'amuser dactylographie. Il n'est peut-être pas incroyablement lisible, mais il fait le travail sans avoir à redéfinir les méthodes statiques dans les classes enfants.

Fonctionne avec [email protected]

Considérer ce qui suit ;

// IModelClass is just here to describe an instanciator
// since we can't use typeof T (unfortunately) with
// the generic type system.
interface IModelClass<T extends Model> {
  new (...a: any[]): T

  // unfortunately, we have to put here again all the typing information
  // of the static members (without static, since we are describing a class, not an instance)

  some_member: string
  create<T extends Model>(this: IModelClass<T>): T
}

class Model {

  // Here we use this with the IModel<T> to force the
  // type system to use T as our current caller.

  static some_member: string

  // When we call Dog.create() below, T is thus resolved
  // to Dog *and stays that way*
  // If typeof worked on generic types (it doesn't), we could have defined this method
  // instead as 
  // static create<T extends Model>(this: typeof T): T { ... }
  static create<T extends Model>(this: IModelClass<T>): T {
    return new this() // whatever you fancy here
  }
}

class Dog extends Model {
  bark() { }
}

class Cat extends Model {
  meow() { }
}

// Everything should be typed here, and we didn't have to redefine static methods
// in Dog nor Cat
let dog = Dog.create()
dog.bark()
let cat = Cat.create()
cat.meow()

@ceymard pourquoi this est-il donné en paramètre?

Cela est dû à https://github.com/Microsoft/TypeScript/issues/3694 qui a été livré avec TypeScript 2.0.0 qui permet de définir un paramètre this pour les fonctions (permettant ainsi l'utilisation de this dans leur corps sans problèmes et pour éviter également une utilisation incorrecte des fonctions)

Il s'avère que nous pouvons les utiliser sur des méthodes ainsi que sur des méthodes statiques, et que cela permet des choses vraiment amusantes, comme l'utiliser pour "aider" l'inférateur de type en le forçant à comparer les this que nous fournissons (IModèle) à celui utilisé (Chien ou Chat). Il « devine » ensuite le type correct pour T et l'utilise, tapant ainsi correctement notre fonction.

(Notez que this est un paramètre _spécial_ compris par le compilateur typescript, il n'en ajoute pas un de plus à la fonction)

Je pensais que la syntaxe était quelque chose comme <this extends Whatever> . Donc, cela fonctionnerait toujours si create() avait d'autres paramètres ?

Absolument

Je voulais également souligner que cela est important pour les types intégrés.
Lorsque vous sous-classez Array par exemple, la méthode from() de la sous-classe renverra une instance de la sous-classe, et non Array .

class Task {}
class TaskList extends Array<Task> {
  public execute() {}
}
// actually returns instance of TaskList at runtime
const tasks = TaskList.from([new Task()])
tasks.execute() // error, method execute does not exist on type Array

Une mise à jour sur ce problème ? Cette fonctionnalité améliorerait considérablement l'expérience de mon équipe avec TypeScript.

Veuillez noter que c'est plutôt critique car c'est un modèle couramment utilisé dans les ORM et beaucoup de code se comporte très différemment de ce que le compilateur et le système de type pensent qu'il devrait.

Je soulèverais des contre-arguments contre des contre-arguments.

Comme @shlomiassaf l' a souligné, autoriser this dans les membres statiques causera de la confusion car this signifie une chose différente dans la méthode statique et la méthode d'instance. De plus, this dans l'annotation de type et this dans le corps de la méthode ont une sémantique différente. Nous pouvons éviter cette confusion en introduisant un nouveau mot-clé, mais le coût d'un nouveau mot-clé est élevé.

Et il existe une solution de contournement simple dans TypeScript actuel. @ceymard l' a déjà montré . Et je préférerais son approche car elle capture la sémantique d'exécution de la méthode statique. Considérez ce code.

class A {
  constructor() {}
  static create<T extends A>(this: {new (): T}) {} // constructor signature is exactly the same as A's
}

class B extends A {
  constructor(a: number) {
    super()
  }
}

B.create() // correctly trigger compile error here

Si le polymorphisme est pris en charge, le compilateur doit signaler une erreur dans class B extends A . Mais c'est un changement radical.

@HerringtonDarkholme Je ne vois pas la confusion, le type de retour de this est exactement ce que j'attendrais de return new this() . Potentiellement, nous pourrions utiliser self pour cela, mais introduire un autre mot-clé (autre que celui utilisé pour produire la sortie) me semble plus déroutant. Nous l'utilisons déjà comme type de retour, le seul (mais gros) problème est qu'il n'est pas pris en charge dans les membres statiques.

Le compilateur ne devrait pas obliger les programmeurs à faire des contournements, TypeScript est censé être un "JavaScript amélioré" et ce n'est pas vrai dans ce cas, vous devez faire un contournement complexe pour que votre code ne génère pas des millions d'erreurs. C'est bien que nous puissions y parvenir dans la version actuelle d'une manière ou d'une autre, mais cela ne signifie pas que tout va bien et qu'il n'y a pas besoin de correctif.

Pourquoi le type de retour de this est-il différent du type de this dans le corps de la méthode ?
Pourquoi ce code échoue-t-il ? Comment l'expliquer à un nouveau venu ?

class A {
  static create(): this {
     return this
  }
}

Pourquoi un sur-ensemble de JavaScript échouerait-il à accepter cela ?

class A {
  static create() {
    return new this()
  }
}

abstract class B extends A {}

Certes, nous devrons peut-être alors introduire un autre mot-clé. Pourrait être self , ou peut-être instance

@HerringtonDarkholme Voici comment je l'explique à un nouveau venu.
Quand tu fais

class A {
  static whatever(): B {
    return new B();
  }
}

le type de retour de B signifie que vous devez retourner une instance de B . Si vous voulez retourner la classe elle-même, vous devez utiliser

class B {
 static whatever(): typeof B {
   return B;
 }
}

Maintenant, tout comme en JavaScript, this dans les membres statiques fait référence à la classe. Donc, si vous utilisez un type de retour de this dans une méthode statique, vous devrez retourner une instance de la classe :

class A {
  static whatever(): this {
    return new this();
  }
}

si vous voulez retourner la classe elle-même, vous devez utiliser typeof this :

class A {
  static whatever(): typeof this {
    return this;
  }
}

c'est exactement comme ça que les types fonctionnent déjà dans TS depuis toujours. Si vous tapez avec une classe, TS attend une instance. Si vous voulez taper pour le type de classe lui-même, vous devez utiliser typeof .

Ensuite, je demanderais pourquoi this n'a pas la même signification dans le corps de la méthode. Si this dans l'annotation de type de la méthode d'instance a la même signification que this dans le corps de la méthode correspondante, pourquoi n'en est-il pas de même pour la méthode statique ?

Nous sommes face à un dilemme et il y a trois options :

  1. make this signifie différentes choses dans différents contextes (confusion)
  2. introduisez un nouveau mot-clé comme self ou static
  3. laisser le programmeur écrire des annotations de type plus étranges (impact sur certains utilisateurs)

Je préfère la troisième approche car elle reflète la sémantique d'exécution de la définition de méthode statique .
Il repère également les erreurs sur le site d'utilisation, plutôt que sur le site de définition, c'est-à-dire que, comme dans ce commentaire , l'erreur est signalée dans B.create , et non class B extends A . Utiliser l'erreur du site est plus précis dans ce cas. (considérez que vous déclarez une méthode statique faisant référence à this puis déclarez une sous-classe abstraite).

Et, plus important encore, il ne nécessite pas de proposition de langage pour une nouvelle fonctionnalité.

En effet, la préférence diffère des personnes. Mais au moins, je veux voir une proposition plus détaillée comme celle -ci. Il y a beaucoup de problèmes à résoudre comme l'assignabilité des classes abstraites, les signatures de constructeurs incompatibles avec les sous-classes et les stratégies de rapport d'erreurs.

@HerringtonDarkholme Parce que dans le cas des méthodes d'instance, le runtime this est une instance d'objet et non une classe, il n'y a donc pas de place pour la confusion. Il est clair que le type this est littéralement le runtime this , il n'y a pas d'autre option. Alors que dans le cas statique, il se comporte comme n'importe quelle autre annotation de classe dans n'importe quel langage de programmation orienté objet : annotez-le avec une classe et vous attendez une instance de retour. De plus, dans le cas de TS, parce que dans JavaScript, les classes sont aussi des objets, tapez-le avec typeof une classe et vous récupérez la classe littérale.

Je veux dire qu'il aurait probablement été plus cohérent à l'époque où ils ont implémenté le type de retour this de penser également au cas statique et d'exiger à la place que les utilisateurs écrivent toujours typeof this pour les méthodes d'instance. Mais cette décision a été prise à l'époque, nous devons donc travailler avec maintenant, et comme je l'ai dit, cela ne laisse pas vraiment de place à la confusion car this dans les méthodes d'instance ne produit aucun autre type (contrairement aux classes, qui avoir un type statique et un type d'instance), il est donc distinct dans ce cas et ne confondra personne.

OK, supposons simplement que personne ne sera confus par this . Qu'en est-il de l'assignabilité des classes abstraites ?

Le caractère statique des méthodes est quelque peu arbitraire. Toute fonction peut avoir des propriétés et des méthodes. S'il peut également être appelé avec new , nous choisissons d'appeler ces propriétés et méthodes static . Maintenant, cette convention a été fermement ancrée dans ECMAScript.

@HerringtonDarkholme , vous avez sans aucun doute raison de dire que l'utilisation de this serait source de confusion. Cependant, comme il n'y a rien d'incorrect à utiliser this , je dirais que c'est parfaitement bien, surtout si l'on tient compte de votre analyse astucieuse des coûts-avantages des différentes alternatives. Je pense que this est la moins mauvaise alternative.

J'ai juste essayé de rattraper ce fil à partir de mon message d'origine, j'ai l'impression que cela a peut-être un peu dérapé.

Pour clarifier, le souhait est que l'annotation de type this soit polymorphe lorsqu'elle est utilisée dans le constructeur, en particulier en tant que générique/modèle. En ce qui concerne @shlomiassaf et @HerringtonDarkholme , je pense que l'exemple avec les méthodes static peut prêter à confusion et ce n'est pas l'intention de ce problème.

Même s'il n'est pas souvent considéré comme tel, le constructeur d'une classe est statique. L'exemple (que je republierai avec des commentaires plus clairs) ne déclare pas this sur une méthode statique, il déclare plutôt this pour une utilisation future via un générique dans l'annotation de type d'une méthode statique .

La distinction étant que je ne veux pas que this calculé immédiatement sur une méthode statique, mais à l'avenir dans une méthode dynamique.

// START LIBRARY CODE
// Constrains the constructor to one that creates things that extend from BaseModel
interface ModelConstructor<T extends BaseModel> {
    new(fac: ModelAPI<T>): T;
}

class ModelAPI<T extends BaseModel> {
    // skipping the use of a ModelConstructor in favor of typeof does not work
    // constructor(private modelType: typeof T) {}
    constructor(private modelType: ModelConstructor<T>) {}

    create() {
        return new this.modelType(this);
    }
}

class BaseModel {
    // This is where "polymorphic `this`" in static members matters. We are 
    // trying to say that the ModelAPI should create instances of whatever 
    // the *current* class is, not the BaseModel class. Much like it would 
    // at runtime.
    constructor(private fac: ModelAPI<this>) {}

    reload() {
        // `reload()` returns a new instance of type Any, incorrect
        return this.fac.create();
    }
}
// END LIBRARY CODE

// START APPLICATION CODE
// Create a custom model class with custom behavior
class Model extends BaseModel {}

// Create an instance of the model API that produces my custom type
let api = new ModelAPI<Model>(Model);  // ModalAPI should be able to infer "<Model>" from the constructor?
let modelInst = api.create();  // Returns type of Model, correct
let reset = modelInst.reload();  // Returns type of Any, incorrect
// END APPLICATION CODE

Pour tous ceux qui pensent que c'est déroutant, eh bien, ce n'est pas très simple, je suis d'accord. Cependant, l'utilisation de this dans le constructeur BaseModel n'est pas vraiment une utilisation statique, c'est une utilisation différée à calculer ultérieurement. Cependant, les méthodes statiques (y compris le constructeur) ne fonctionnent pas de cette façon.

Je pense qu'au moment de l'exécution, tout fonctionne comme prévu. Cependant, le système de types est incapable de modéliser ce modèle particulier. L'incapacité pour le tapuscrit de modéliser un modèle javascript est à la base de la raison pour laquelle ce problème est ouvert.

Désolé si c'est un commentaire déroutant, écrit à la hâte.

@xealot Je comprends votre point, mais d'autres problèmes qui suggéraient spécifiquement this polymorphe pour les méthodes statiques ont été fermés en double de celui-ci. Je suppose que le correctif dans TS permettrait les deux cas d'utilisation.

Quel est votre cas d'utilisation exact ? Peut-être que la solution "cette" est suffisante.

Je l'utilise dans une bibliothèque ORM personnalisée avec succès
Le jeu. 20 oct. 2016 à 19:15, Tom Marius [email protected] a
écrit :

Veuillez noter que c'est plutôt critique puisqu'il s'agit d'un outil couramment utilisé
modèle dans les ORM et beaucoup de code se comporte très différemment du
le compilateur et le système de type pensent qu'il le devrait.


Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -255169194,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AAtAoRHc45B386xdNhuCLB8iW6i82e7Uks5q16GzgaJpZM4GsmOH
.

Merci à @ceymard et @HerringtonDarkholme.

static create<T extends A>(this: {new (): T}) { return new this(); } a fait l'affaire pour moi 👍

C'est plus obscur que static create(): this { return new this(); } à première vue mais au moins, c'est correct ! Il capture avec précision le type de this dans la méthode statique.

J'espère que cela pourra être priorisé, même si ce n'est pas le plus voté ou commenté, etc. C'est très ennuyeux de devoir utiliser un argument de type pour obtenir le bon type, par exemple:

let u = User.create<User>();

Lorsqu'il est pratiquement possible de faire simplement :

let u = User.create();

Et il est inévitable que static fasse parfois référence à des types d'instances.

J'ai eu une autre idée concernant la discussion sur la question de savoir si le type this dans les membres statiques devrait être le type du côté statique de la classe ou du côté instance. this pourrait être le côté statique, cela correspondrait à ce qui est fait dans les membres de l'instance et quel est le comportement d'exécution, puis vous pourriez obtenir le type d'instance via this.prototype :

abstract class Model {
  static findAll(): Promise<this.prototype[]>
}

class User extends Model {}

const users: User[] = await User.findAll();

ce serait conforme à la façon dont cela fonctionne en JavaScript. this est l'objet de classe lui-même, et l'instance est sur this.prototype . C'est essentiellement le même mécanisme que les types mappés, équivalent à this['prototype'] .

De la même manière, vous pouvez imaginer que le côté statique soit disponible dans les membres de l'instance via this.constructor (je ne peux pas penser à un cas d'utilisation pour cet atm).

Je voulais également mentionner qu'aucune des solutions de contournement / hacks mentionnées ne fonctionne pour taper le modèle ORM dans Sequelize.

  • L'annotation de type this ne peut pas être utilisée dans un constructeur, elle ne fonctionne donc pas pour cette signature :
    ts abstract class Model { constructor(values: Partial<this.prototype>) }
  • Passer le type en tant qu'argument de classe générique ne fonctionne pas pour les méthodes statiques, car les membres statiques ne peuvent pas faire référence à des paramètres génériques
  • Passer le type en tant qu'argument de méthode générique fonctionne pour les méthodes, mais pas pour les propriétés statiques, comme :
    ts abstract class Model { static attributes: { [K in keyof this.prototype]: { type: DataType, field: string, unique: boolean, primaryKey: boolean, autoIncrement: boolean } }; }

+1 pour cette demande. mon ORM sera beaucoup plus propre.

J'espère que nous verrons bientôt la solution propre

En attendant, merci à tous ceux qui ont proposé des solutions !

Pouvons-nous aller plus loin sur ce sujet puisqu'il n'y a plus de discussion?

Où voudriez-vous le prendre? C'est toujours une façon dont Typescript ne peut pas modéliser proprement Javascript pour autant que je sache et je n'ai pas vu de solution autre que simplement ne pas le faire.

Il s'agit d'une fonctionnalité indispensable pour les développeurs ORM, car nous pouvons déjà voir que trois propriétaires d'ORM populaires ont demandé cette fonctionnalité.

@pleerock @xealot Des solutions ont été proposées ci-dessus :

export type StaticThis<T> = { new (): T };

export class Base {
    static create<T extends Base>(this: StaticThis<T>) {
        const that = new this();
        return that;
    }
    baseMethod() { }
}

export class Derived extends Base {
    derivedMethod() { }
}

// works
Base.create().baseMethod();
Derived.create().baseMethod();
// works too
Derived.create().derivedMethod();
// does not work (normal)
Base.create().derivedMethod();

Je l'utilise abondamment. Les déclarations des méthodes statiques dans les classes de base sont un peu lourdes mais c'est le prix à payer pour éviter la distorsion sur le type de this à l'intérieur des méthodes statiques.

@pleerock

J'ai un ORM interne qui utilise largement le modèle this: sans problème.

Je pense qu'il n'est pas nécessaire de surcharger la langue lorsque la fonctionnalité est en fait déjà là, bien qu'elle soit certes un peu alambiquée. Le cas d'utilisation d'une syntaxe plus claire est assez limité à mon humble avis et peut introduire des incohérences.

Peut-être qu'il pourrait y avoir un fil Stackoverflow avec cette question et des solutions de référence ?

@bjouhier @ceymard J'ai expliqué pourquoi toutes les solutions de contournement de ce fil ne fonctionnent pas pour Sequelize : https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -269463313

Et cela ne concerne pas seulement les ORM, mais aussi la bibliothèque standard : https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -244550725

@felixfbecker Je viens de publier quelque chose sur le cas d'utilisation du constructeur et de le supprimer (j'ai réalisé que TS n'impose pas de constructeurs dans chaque sous-classe juste après la publication).

@felixbecker En ce qui concerne le cas du constructeur, je le résous en m'en tenant à des constructeurs sans paramètres et en fournissant à la place des méthodes statiques spécialisées (créer, cloner, ...). Plus explicite et plus facile à taper.

@bjouhier Cela signifierait que vous devez redéclarer essentiellement chaque méthode dans chaque sous-classe de modèle. Regardez combien il y en a dans Sequelize : http://docs.sequelizejs.com/class/lib/model.js~Model.html

Mon argument est d'un point de vue complètement différent. Bien qu'il existe des solutions de contournement partielles et quelque peu peu intuitives et que nous puissions discuter et être en désaccord sur le point de savoir si elles sont suffisantes ou non, nous pouvons convenir qu'il s'agit d'un domaine où Typescript ne peut pas facilement modéliser Javascript.

Et ma compréhension est que Typescript devrait être une extension de Javascript.

@felixfbecker

Cela signifierait que vous devez redéclarer essentiellement chaque méthode dans chaque sous-classe de modèle.

Pourquoi? Ce n'est pas du tout mon expérience. Pouvez-vous illustrer le problème avec un exemple de code ?

@bjouhier J'ai illustré le problème en profondeur avec des exemples de code dans mes commentaires dans ce fil, pour commencer https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -222348054

Mais regardez mon exemple create ci-dessus . La méthode create est une méthode statique. Il n'est pas redéclaré et pourtant il est correctement typé dans la sous-classe. Pourquoi les méthodes Model auraient-elles besoin d'une redéfinition alors ?

@felixfbecker Votre exemple _pour commencer_ :

export type StaticThis<T> = { new (): T };

abstract class Model {
    public static tableName: string;
    public static findById<T extends Model>(this: StaticThis<T>, id: number): T {
        const instance = new this();
        // details omitted
        return instance;        
    }
}

class User extends Model {
    public static tableName = 'users';
    public username: string;
}

const user = User.findById(1); 
console.log(user.username);

@bjouhier D'accord, il semble donc que les annotations this: { new (): T } fassent réellement fonctionner l'inférence de type. Ce qui me fait me demander pourquoi ce n'est pas le type par défaut utilisé par le compilateur.
La solution de contournement ne fonctionne bien sûr pas pour le constructeur, car ceux-ci ne peuvent pas avoir de paramètre this .

Oui, cela ne fonctionne pas pour les constructeurs, mais vous pouvez contourner le problème avec un petit changement d'API, en ajoutant une méthode statique create .

Je comprends cela, mais il s'agit d'une bibliothèque JavaScript, nous parlons donc de taper l'API existante dans une déclaration.

Si this signifiait { new (): T } dans une méthode statique, alors this ne serait pas le bon type pour this dans la méthode statique. Par exemple, new this() ne serait pas autorisé. Pour des raisons de cohérence, this doit être le type de la fonction constructeur, et non le type de l'instance de classe.

J'ai le problème de taper une bibliothèque existante. Si vous n'avez pas le contrôle sur cette bibliothèque, vous pouvez toujours créer une classe de base intermédiaire ( BaseModel extends Model ) avec une fonction create correctement typée et dériver tous vos modèles de BaseModel .

Si vous souhaitez accéder aux propriétés statiques de la classe de base, vous pouvez utiliser

public static findById<T extends Model>(this: (new () => T) & typeof Model, id: number): T {...}

Mais vous avez probablement un point valable sur le constructeur. Je ne vois pas de raison difficile pour laquelle le compilateur doit rejeter ce qui suit :

constructor(values: Partial<this>) {}

Je suis d'accord avec @xealot qu'il y a de la douleur ici. Ce serait tellement cool si nous pouvions écrire

static findById(id: number): instanceof this { ... }

à la place de

static findById<T extends Model>(this: (new () => T), id: number): T { ... }

Mais nous avons besoin d'un nouvel opérateur dans le système de typage ( instanceof ?). Ce qui suit n'est pas valide :

static findById(id: number): this { ... }

TS 2.4 a cassé ces solutions de contournement pour moi :
https://travis-ci.org/types/sequelize/builds/247636686

@sandersn @felixfbecker Je pense que c'est un bogue valide. Cependant, je ne peux pas le reproduire de façon minimale. Le paramètre de rappel est contravariant.

// Hooks
User.afterFind((users: User[], options: FindOptions) => {
  console.log('found');
});

Une chance que cela soit corrigé à un moment donné ?

J'ai rencontré cela aujourd'hui aussi. Fondamentalement, je voulais construire une classe de base singleton, comme ceci:

abstract class Singleton<T> {
  private static _instance?: T

  public static function getInstance (): T {
    return this._instance || (this._instance = new T())
  }
}

L'utilisation serait quelque chose comme:

class Foo extends Singleton<Foo> {
  bar () {
    console.log('baz!')
  }
}

Foo.getInstance().bar() // baz!

J'ai essayé environ 10 variantes de ceci, avec la variante StaticThis mentionnée ci-dessus, et bien d'autres. En fin de compte, il n'y a qu'une seule version qui compilerait même, mais le résultat de getInstance() a été dérivé comme juste Object .

J'ai l'impression que c'est beaucoup plus difficile que nécessaire pour travailler avec des constructions comme celles-ci.

Travaux suivants :

class Singleton {
    private static _instance?: Singleton;

    static getInstance<T extends Singleton>(this: { new(): T }) {
      const constr = this as any as typeof Singleton; // hack
      return (constr._instance || (constr._instance = new this())) as T;
    }
  }

  class Foo extends Singleton {
    foo () { console.log('foo!'); }
  }

  class Bar extends Singleton {
    bar () { console.log('bar!');}
  }

  Foo.getInstance().foo();
  Bar.getInstance().bar();

La partie this as any as typeof Singleton est moche mais elle indique que nous trompons le système de type car _instance doit être stocké sur le constructeur de la classe dérivée.

Typiquement, la classe de base sera enterrée dans votre framework donc ça ne nuit pas beaucoup si son implémentation est un peu moche. Ce qui compte, c'est un code propre dans les classes dérivées.

J'espérais en 2.8 nightlies pouvoir faire quelque chose comme ça:

static findById(id: number): InstanceType<this> { ... }

Malheureusement pas de chance :(

Les exemples de code @bjouhier fonctionnent comme un charme. Mais cassez la saisie semi-automatique IntelliSense.

React 16.3.0 a été publié et il semble impossible de taper correctement la nouvelle méthode getDerivedStateFromProps , étant donné qu'une méthode statique ne peut pas référencer les paramètres de type de classe :

(exemple simplifié)

class Component<P, S> {

    static getDerivedStateFromProps?<K extends keyof S>(nextProps: P, prevState: S): Pick<S, K> | null

    props: P
    state: S
}

Y a-t-il une solution de contournement? (Je ne compte pas "taper P et S comme PP et SS et j'espère que les développeurs feront correctement correspondre ces deux familles de types" comme une seule : p)

PR : https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24577

@cncolder J'ai également été témoin de la rupture d'intellisense. Je crois que cela a commencé avec le tapuscrit 2.7.

Une chose que je ne vois pas ici, s'étendant sur l'exemple Singleton - ce serait un modèle courant de faire en sorte qu'une classe Singleton / Multiton ait un constructeur protégé - et cela ne peut pas être étendu si le type est { new(): T } comme ceci applique un constructeur public - le terme this ou une autre solution fournie ici ( instanceof this , typeof this etc) devrait résoudre ce problème. Ceci est certainement possible en lançant une erreur dans le constructeur si le singleton n'a pas demandé sa création, etc., mais cela va à l'encontre du but des erreurs de frappe - pour avoir une solution d'exécution ...

class Singleton {

  private static _instance?: Singleton;

  public static getInstance<T extends Singleton> ( this: { new(): T } ): T {
    const ctor: typeof Singleton = this as any; // hack
    return (ctor._instance || (ctor._instance = new this())) as T;
  }

  protected constructor ( ) { return; } 
}

class A extends Singleton {
  protected constructor ( ) {
    super();
  }
}

A.getInstance() // fails because constructor is not public

Je ne sais pas pourquoi vous auriez besoin d'un singleton dans TS avec getInstance() , vous pouvez simplement définir un objet littéral. Ou si vous voulez absolument utiliser des classes (pour les membres privés), exportez uniquement l'instance de classe et non la classe, ou utilisez une expression de classe ( export new class ... ).

C'est plus utile pour Multiton - et je crois que j'ai trouvé un modèle qui fonctionne,... même si c'est toujours très hacky sans ce type... c'est plutôt nécessaire

this -> { new(): T } & typeof Multiton | Function

class Multiton {
  private static _instances?: { [key: string]: any };

  public static getInstance<T extends Multiton> (
    this: { new(): T } & typeof Multiton | Function, key: string
  ): T {
    const instances: { [key: string]: T } =
      (this as typeof Multiton)._instances ||
     ((this as typeof Multiton)._instances = { });

    return (instances[key] ||
      (instances[key] = new (this as typeof Multiton)() as T)
    );
  }

  protected constructor ( ) { return; }
}

class A extends Multiton {
  public getA ( ): void { return; }
}
A.getInstance("some-key").getA();
assert(A.getInstance("some-key") === A.getInstance("some-key"))
new A(); // type error, protected constructor

J'ai aussi du code pour stocker la clé afin qu'elle soit accessible dans la classe enfant lors de la création, mais je l'ai supprimée pour plus de simplicité...

Salut. Je rencontre un problème qui nécessite "this" polymorphe pour le constructeur. Similaire à ce commentaire.

Je veux créer une classe. La classe elle-même a de nombreuses propriétés, de sorte que le constructeur n'aura pas l'air bien si toutes les propriétés de cette classe sont initialisées via les paramètres du constructeur.

Je préfère un constructeur qui accepte un objet pour initialiser les propriétés. Par example:

class Vehicle {
  // many properties here
  // ...

  constructor(value: Partial<Vehicle>) {
      Object.assign(this, value)
  }
}

let vehicle = new Vehicle({
  // <-- IntelliSense works here
})

Ensuite, je veux étendre la classe. Par example:

class Car extends Vehicle {
  // more properties here
  // ...
}

let car = new Car({
  // <-- I can't infer Car's properties here
})

Ce serait très bien si le type this pouvait aussi être utilisé sur le constructeur.

class Vehicle {
  // ...
  constructor(value: Partial<this>) {
      // ...

Ainsi, IntelliSense peut déduire les propriétés de la classe enfant sans passe-partout supplémentaire : par exemple, redéfinir le constructeur. Cela semblera plus naturel que d'utiliser une méthode statique pour initialiser la classe.

let car = new Car({
  // <-- Car's properties will be able to be inferred if Partial<this> is allowed
})

Merci beaucoup pour votre considération.

Je suis content d'avoir trouvé ce fil. Je pense honnêtement que c'est un problème très grave et j'ai été très découragé de le voir se poursuivre jusqu'à la version 3.0.

En examinant toutes les solutions et en tenant compte des diverses solutions de contournement avec lesquelles j'ai joué au cours des deux dernières années, j'arrive à la conclusion que toute solution rapide que nous essayons échouera simplement si et quand une solution est en place. Et jusque-là, chaque contrainte que nous effectuons nécessite simplement trop de travail manuel pour la maintenir tout au long du projet. Et une fois publié, vous ne pouvez pas demander aux consommateurs de comprendre votre propre état d'esprit et pourquoi vous avez choisi de réparer quelque chose qui est TypeScript s'ils consomment simplement votre package en vanille mais comme la plupart d'entre nous, travaillent avec VSCode.

Dans le même temps, écrire du code pour rendre le système de type heureux lorsque la langue réelle n'est pas le problème va à l'encontre de la même raison d'utiliser les fonctionnalités typées pour commencer.

Ma recommandation à tout le monde est d'accepter que les gribouillis sont défectueux et d'utiliser //@ts-ignore because my code works as expected lorsque cela est possible.

Si votre linter est à l'abri de l'intelligence humaine raisonnable, il est temps d'en trouver un qui connaît sa place.

MISE À JOUR :
Je voulais ajouter ma propre tentative pour augmenter peut-être en toute sécurité l'inférence statique. Cette approche est strictement ambiante car elle est destinée à être à l'écart. Il est exhaustivement explicite, vous obligeant à vérifier vos augmentations et à ne pas simplement supposer que l'héritage fera le travail pour vous.

export class Sequence<T> {
  static from(...values) {
    // … returns Sequence<T>
  };
}

export class Peekable<T> extends Sequence<T> {
  // no augmentations needed in actual class body
}

/// AMBIENT /// Usually keep those at the bottom of my files

export declare namespace Peekable {
  export function from<T>(... values: T[]): Peekable<T>;
}

Évidemment, je ne peux pas garantir que ce modèle tient, ni qu'il satisferait tous les scénarios de ce fil, mais pour l'instant, cela fonctionne comme prévu.

Ces choix découlent de la prise de conscience inquiétante que jusqu'à présent, les propres fichiers lib de TypeScript n'incluent que deux déclarations de classe !

SafeArrayName

/**
 * Represents an Automation SAFEARRAY
 */
declare class SafeArray<T = any> {
    private constructor();
    private SafeArray_typekey: SafeArray<T>;
}

DateVar

/**
 * Automation date (VT_DATE)
 */
declare class VarDate {
    private constructor();
    private VarDate_typekey: VarDate;
}

Discuté pendant un certain temps aujourd'hui. Points clés:

  • Est-ce que this fait référence au côté instance ou au côté statique ?

    • Certainement le côté statique. Le côté instance peut être référencé via InstanceTypeOf mais l'inverse n'est pas vrai. Cela maintient également la symétrie selon laquelle this dans la signature a le même type que this dans le corps.

  • Problèmes de solidité

    • Il n'y a aucune garantie qu'une liste de paramètres de constructeur de classe dérivée ait une relation avec sa liste de paramètres de constructeur de classe de base

    • Les classes cassent extrêmement souvent la substituabilité côté statique à cet égard

    • La substituabilité non côté construction est appliquée et c'est généralement ce qui intéresse les gens de toute façon

    • Nous autorisons déjà les invocations erronées de new this() dans les méthodes static

    • Probablement personne ne se soucie du côté signature de construction de this statique de l'extérieur ?

Solutions de contournement existantes :

class Foo {
    static boo<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();

Notamment, cela ne fonctionne que si la signature de construction Bar est correctement substituable à celle de Foo

@RyanCavanaugh J'ai essayé de trouver une explication d'où vient la valeur de this dans votre exemple static boo<T extends typeof Foo>(this: T): InstanceType<T> , mais je ne comprends tout simplement pas. Bien sûr, j'ai tout relu sur les génériques, mais quand même - non. Si je remplace this par clazz par exemple, cela ne fonctionnera plus et le compilateur se plaindra avec "Expected 1 arguments, but got 0". Donc un peu de magie s'opère là-bas. Pourriez-vous expliquer? Ou m'indiquer la bonne direction dans la documentation ?

Éditer:
Aussi, pourquoi est-ce extends typeof Foo et pas simplement extends Foo ?

@creynders this est un faux paramètre spécial qui peut être saisi pour le contexte de la fonction. docs .

L'utilisation InstanceType<T> ne fonctionne pas lorsque des génériques sont impliqués.

Mon cas d'utilisation ressemble à ceci :

const _data = Symbol('data');

class ModelBase<T> {
    [_data]: Readonly<T>;

    protected constructor(data: T) {
        this[_data] = Object.freeze(data);
    }


    static create<T, V extends typeof ModelBase>(this: V, data : T): InstanceType<V> {
        return new this(data);
    }
}

interface IUserData {
    id: number;
}

class User extends ModelBase<IUserData> {}

User.create({ id: 5 });

Lien de terrain de jeu TypeScript

@mastermatt oh wow, je n'ai absolument PAS compris cela lors de la lecture des documents ... Merci

En regardant l'exemple de @RyanCavanaugh , j'ai pu faire fonctionner nos méthodes statiques, mais j'ai aussi des méthodes d'instance qui font référence aux méthodes statiques, mais je n'arrive pas à comprendre comment obtenir une frappe correcte pour cela.

Exemple:

class Foo {
    static boo<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }

    boo<T extends typeof Foo>(this: InstanceType<T>): InstanceType<T> {
      return (this.constructor as T).boo();
    }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();
// c: Foo
let c = b.boo();

Si je pouvais trouver une solution de contournement pour ce dernier problème, je serais prêt jusqu'à ce que quelque chose d'officiel arrive.

A noter également avec cet exemple, si la sous-classe essaie de remplacer une méthode statique puis d'appeler super , alors tout est également bouleversé ... À part ces deux problèmes, la solution de contournement semble bien fonctionner. Bien que ces deux problèmes soient plutôt bloquants pour notre code.

IIRC c'est parce que cela n'est pas exprimable en tapuscrit sans tordre un peu le système de type. Un péché ; this.constructor n'est pas du tout garanti d'être ce que vous pensez qu'il est ou quelque chose comme ça.

Dans ce cas précis, j'aurais la méthode d'instance boo() qui retournerait this et tricherait un peu à l'intérieur, forçant le compilateur à accepter que je sais ce que je fais.

Mon raisonnement général est que je veux que l'API soit la plus simple possible pour le reste du code, quitte à parfois tricher un peu.

Si quelqu'un a quelque chose de plus robuste, j'aimerais bien

class Foo {
  static boo<T extends typeof Foo>(this: T): InstanceType<T> {
      return (new this()) as InstanceType<T>;
  }

  boo(): this {
    // @ts-ignore : wow this is ugly 
    return (this.constructor).boo();
  }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();
// c: Bar !
let c = b.boo();

Vous pouvez également suivre la route de l'interface et faire quelque chose comme ça ;

interface FooMaker<T> {
  new(...a: any[]): T
  boo(): T
}


class Foo {
  static boo<T extends typeof Foo>(this: T): InstanceType<T> {
      return (new this()) as InstanceType<T>;
  }

  boo(): this {
    return (this.constructor as FooMaker<this>).boo();
  }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();
// c: Bar !
let c = b.boo();

C'est de la triche aussi, mais peut-être un peu plus contrôlée ? Je ne peux pas vraiment dire.

Ainsi, le 1er argument this dans la méthode statique "boo" est traité spécialement, pas comme l'argument normal ? Des liens vers des documents décrivant cela ?

class Foo {
    static boo<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();

J'ai le même problème (au moins similaire).
J'utilise Vanilla Javascript avec JSDoc (pas TypeScript), donc je ne peux pas utiliser/implémenter les solutions de contournement autour des génériques proposées dans ce fil, mais la racine semble être la même.
J'ai ouvert ce numéro : #28880

Ce problème a littéralement 3 ans déjà.
Est-ce que quelqu'un a trouvé une solution de contournement appropriée qui pourrait fonctionner pour moi?

Trois ans

Le travail de @RyanCavanaugh est idéal pour les méthodes statiques, mais qu'en est-il d'un accesseur statique ?

class Factory<T> {
  get(): T { ... }
}

class Base {
  static factory<T extends Base>(this: Constructor<T>): Factory<T> {
    //
  }

  // what about a getter?
  static get factory<** no generics allowed for accessors **> ...
}

Trois ans

D'accord, vous êtes à moitié doué pour soustraire des dates. Mais pouvez-vous proposer une solution ? ;)

@RyanCavanaugh C'est un problème lorsque nous utilisons la this dans les générateurs comme suit :

class C {
  constructor(f: (this: this) => void) {
  }
}
new C(function* () {
  this;
  yield;
});

Lorsque nous fabriquons des générateurs, nous ne pouvons pas utiliser les fonctions fléchées pour utiliser la this . Nous devons donc maintenant déclarer explicitement le type this dans les sous-classes. Un cas réel est le suivant :

      class Component extends Coroutine<void> implements El {
        constructor() {
          super(function* (this: Component) {
            while (true) {
              yield;
            }
          }, { size: Infinity });
        }
        private readonly dom = Shadow.section({
          style: HTML.style(`ul { width: 100px; }`),
          content: HTML.ul([
            HTML.li(`item`)
          ] as const),
        });
        public readonly element = this.dom.element;
        public get children() {
          return this.dom.children.content.children;
        }
        public set children(children) {
          this.dom.children.content.children = children;
        }
      }

https://github.com/falsandtru/typed-dom/blob/v0.0.134/test/integration/package.ts#L469

Nous n'avons pas besoin de déclarer le type this dans les sous-classes si nous pouvons le faire dans les classes de base. Cependant, la this n'est pas initialisée lorsque le générateur est appelé de manière synchrone dans la super classe. J'ai évité ce problème dans le code mais c'est un hack sale. Ainsi, ce modèle peut à l'origine ne pas correspondre aux langages de programmation basés sur les classes.

Pas le plus propre, mais peut être utile pour accéder à la classe enfant à partir d'une méthode statique.

class Base {
    static foo<T extends typeof Base>() {
        let ctr = Object.create(this.prototype as InstanceType<T>).constructor;
        // ...
    }
}

class C extends Base {
}

C.foo();

Bien que je ne sois pas sûr que le problème auquel je suis actuellement confronté soit dû à cela, mais après un bref aperçu de ce problème, il semble que ce soit probablement le cas. Merci de corriger si ce n'est pas le cas.

J'ai donc les 2 classes suivantes liées par héritage.

export class Target {
  public static create<T extends Target = Target>(that: Partial<T>): T {
    const obj: T = Object.create(this.prototype);
    this.mapObject(obj, that);
    return obj;
  }
  public static mapObject<T extends Target = Target>(obj: T, that: Partial<T>) {
    // works with "strictNullChecks": false
    obj.prop1 = that.prop1;
    obj.prop2 = that.prop2;
  }

  public prop1!: string;
  constructor(public prop2: string) {}
}

export class SubTarget extends Target {
  public subProp!: string;
}

Ensuite, j'ajoute une méthode mapObject #$1$#$ dans la classe SubTarget comme suit.

  public static mapObject(obj: SubTarget, that: Partial<SubTarget>) {
    super.mapObject(obj, that);
    obj.subProp = that.subProp;
  }

Bien que je m'attendais à ce que cela fonctionne, j'obtiens l'erreur suivante.

Class static side 'typeof SubTarget' incorrectly extends base class static side 'typeof Target'.
  Types of property 'mapObject' are incompatible.
    Type '(obj: SubTarget, that: Partial<SubTarget>) => void' is not assignable to type '<T extends Target>(obj: T, that: Partial<T>) => void'.
      Types of parameters 'obj' and 'obj' are incompatible.
        Type 'T' is not assignable to type 'SubTarget'.
          Property 'subProp' is missing in type 'Target' but required in type 'SubTarget'.

Cette erreur est-elle générée en raison de ce problème ? Sinon, une explication serait vraiment super.

Lien vers l'aire de jeux

Je suis venu ici en cherchant une solution pour annoter les méthodes statiques qui renvoient de nouvelles instances de la classe réelle qui sont appelées.

Je pense que la solution de contournement suggérée par @SMotaal (en combinaison avec new this() ) est la plus propre et la plus logique pour moi. Cela permet d'exprimer l'intention souhaitée, ne m'oblige pas à spécifier le type à chaque appel à la méthode générique et n'ajoute aucune surcharge dans le code final.

Mais sérieusement, comment cela ne fait-il pas encore partie du noyau Typescript? C'est un scénario assez courant.

@danielvy - Je pense que la déconnexion entre la POO et les prototypes est le plus grand écart mental qui ne rend pas tout le monde heureux. " types" à mon avis. Ne pas avoir de solution meilleure que la concurrence n'est un problème que s'il y a concurrence, c'est uniquement pourquoi tous les œufs dans le même panier sont toujours mauvais pour tout le monde - je suis pragmatique et sincère ici.

Cela ne fonctionne pas :

export class Base {
  static getEntitySchema<T extends typeof Base>(
    this: T,
  ): InstanceType<T> {
  }
}

export class Extension extends Base {
  static member: string = '';
  static getEntitySchema<T extends typeof Extension>(
    this: T,
  ): InstanceType<T> {
  }
}

Type 'typeof Base' is missing the following properties from type 'typeof Extension ': member.

Mais cela ne devrait-il pas être autorisé puisque l'extension étend la base ?

Notez que cela fonctionne :

export class Extension extends Base {
  static member: string = '';
  static getEntitySchema<T extends typeof Base>(
    this: T,
  ): InstanceType<T> {
  }
}

mais alors vous ne pouvez pas utiliser this.member à l'intérieur.

Donc, je suppose d'après @ntucker post que la solution de contournement ne fonctionne qu'à un niveau de profondeur à moins que la classe étendue ne corresponde exactement à la classe de base?

Salut! Qu'en est-il des constructeurs protégés ?

class A {
  static create<T extends A>(
    this: {new(): T}
  ) {
    return new this();
  }

  protected constructor() {}
}

class B extends A {} 

B.create(); // Error ts(2684)

Si le constructeur est public - c'est ok, mais sinon, cela provoque une erreur :

The 'this' context of type 'typeof B' is not assignable to method's 'this' of type '(new () => B) & typeof A'.
  Type 'typeof B' is not assignable to type 'new () => B'.
    Cannot assign a 'protected' constructor type to a 'public' constructor type.

Aire de jeux - http://tiny.cc/r74c9y

Y a-t-il un espoir pour cela? Je viens de tomber sur ce besoin une fois de plus :F

@ntucker Duuuuudee, tu l'as fait !!! La réalisation clé pour moi a été de rendre la méthode de création statique générique, de déclarer le paramètre this , puis de faire le cast InstanceType<U> à la fin.

Cour de récréation

D'en haut ^

class Base<T> {
  public static create<U extends typeof Base>(
    this: U
  ) {
    return new this() as InstanceType<U>
  }
}
class Derived extends Base<Derived> {}
const d: Derived = Derived.create() // works 😄 

Cela satisfait parfaitement mon cas d'utilisation, et semble qu'à partir de TS 3.5.1, cela satisfait également la majorité des personnes dans ce fil (voir terrain de jeu pour l'exploration de la prop statique theAnswer ainsi que le 3ème niveau d'extension)

Prise à chaud : je pense que cela fonctionne maintenant et peut être fermé ?

@jonjaques Btw vous n'utilisez jamais T. De plus, cela ne résout pas le problème que j'ai décrit qui est de remplacer les méthodes.

J'ai l'impression que la solution mentionnée a déjà été suggérée il y a presque 4 ans… Et pourtant, ce n'est pas quelque chose qui résout le problème.

J'ai ouvert un stackoverflow pour voir s'il y avait d'autres solutions de contournement que quelqu'un pourrait connaître et quelqu'un avait un bon résumé du problème que je rencontre:

"Les génériques utilisés comme paramètres de méthode, y compris ces paramètres, semblent être contravariants, mais en réalité, vous voulez toujours que ces paramètres soient traités comme covariants (ou peut-être même bivariants)."

Il semble donc que ce soit vraiment quelque chose qui est cassé dans TypeScript lui-même et qui devrait être corrigé ("ceci" devrait être covariant ou bivariant).

EDIT: Ce commentaire a une meilleure façon.

Je n'ai pas grand chose à ajouter, mais cela a bien fonctionné pour moi de https://github.com/microsoft/TypeScript/issues/5863#issuecomment -437217433

class Foo {
    static create<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }
}

class Bar extends Foo { }

// typeof b is Bar.
const b = Bar.create()

J'espère que cela sera utile à tous ceux qui ne veulent pas parcourir ce long numéro.

Un autre cas d'utilisation est les fonctions de membre de garde de type personnalisées sur des non-classes :

type Baz = {
    type: "baz"
}
type Bar = {
     type: "bar"
}
type Foo = (Baz|Bar)&{
    isBar: () => this is Bar 
}

La solution de contournement m'amène presque là où je dois être, mais j'ai une clé de plus à lancer dans les engrenages. Prenons l'exemple artificiel suivant :

interface IAutomobileOptions {
  make: string
}

interface ITruckOptions extends IAutomobileOptions {
  bedLength: number
}

export class Automobile<O extends IAutomobileOptions> {
  constructor(public options: O) {}

  static create<T extends typeof Automobile, O extends IAutomobileOptions>(
    this: T,
    options: O
  ): InstanceType<T> {
    return new this(options) as InstanceType<T>
  }
}

export class Truck<O extends ITruckOptions> extends Automobile<O> {
  constructor(truckOptions: O) {
    super(truckOptions)
  }
}

const car = Automobile.create({ make: 'Audi' })
const truck = Truck.create({ make: 'Ford', bedLength: 7 }) // TS Error on Truck

Voici l'erreur :

The 'this' context of type 'typeof Truck' is not assignable to method's 'this' of type 'typeof Automobile'.
  Types of parameters 'truckOptions' and 'options' are incompatible.
    Type 'O' is not assignable to type 'ITruckOptions'.
      Property 'bedLength' is missing in type 'IAutomobileOptions' but required in type 'ITruckOptions'.ts(2684)

Cela a du sens pour moi, car la méthode create dans la classe Automobile n'a aucune référence à ITruckOptions à O , mais je me demande s'il y a un solution de contournement pour ce problème?

En règle générale, les options de ma classe d'extension pour le constructeur étendront l'interface d'options de la classe de base, donc je sais qu'elles incluront toujours les paramètres de la classe de base, mais je n'ai pas de moyen fiable de s'assurer qu'elles incluent les paramètres pour l'extension classer.

J'ai également dû recourir à des méthodes de remplacement pour étendre les classes afin de les informer des types d'entrée et de retour attendus de la classe héritante, ce qui semble un peu malodorant.

@Jack-Barry ça marche :

interface IAutomobileOptions {
  make: string
}

interface ITruckOptions extends IAutomobileOptions {
  bedLength: number
}

export class Automobile<O extends IAutomobileOptions> {
  constructor(public options: O) { }

  static create<T extends Automobile<O>, O extends IAutomobileOptions>(
    this: { new(options: O): T; },
    options: O
  ): T {
    return new this(options)
  }
}

export class Truck<O extends ITruckOptions> extends Automobile<O> {
  constructor(truckOptions: O) {
    super(truckOptions)
  }
}

const car = Automobile.create({ make: 'Audi' });
const truck = Truck.create({ make: 'Ford', bedLength: 7 });

Cependant, ce n'est toujours pas idéal car les constructeurs sont publics, il est donc possible de faire simplement new Automobile({ make: "Audi" }) .

@elderapo Je pense que cela me donne ce dont j'ai besoin - l'exemple est bien sûr artificiel mais je vois où mon manque de compréhension des _Generics_ m'a un peu mordu. Merci de l'avoir éclairci !

J'espère que cela aidera les autres, faites-moi savoir s'il y a place à l'amélioration. Ce n'est pas parfait, car les fonctions de lecture pourraient potentiellement se désynchroniser avec les typages, mais c'est le plus proche que j'ai pu obtenir du modèle dont nous avions besoin.

// Interface to ensure attributes that exist on all descendants
interface IBaseClassAttributes {
  foo: string
}

// Type to provide inferred instance of class
type ThisClass<
  Attributes extends IBaseClassAttributes,
  InstanceType extends BaseClass<Attributes>
> = {
  new (attributes: Attributes): InstanceType
}

// Constructor uses generic A to assign attributes on instances
class BaseClass<A extends IBaseClassAttributes = IBaseClassAttributes> {
  constructor(public attributes: A) {}

  // this returns an instance of type ThisClass
  public static create<A extends IBaseClassAttributes, T extends BaseClass<A>>(
    this: ThisClass<A, T>,
    attributes: A
  ): T {
    // Perform db creation actions here
    return new this(attributes)
  }

  // Note that read function is a place where typechecking could fail you if db
  //   return value does not match
  public static read<A extends IBaseClassAttributes>(id: string): A | null {
    // Perform db retrieval here assign to variable
    const dbReturnValue = {} as A | null
    return dbReturnValue
  }
}

interface IExtendingClassAttributes extends IBaseClassAttributes {
  bar: number
}

// Extend the BaseClass with the extending attributes interface
class ExtendingClass extends BaseClass<IExtendingClassAttributes> {}

// BaseClass
const bc: BaseClass = BaseClass.create({ foo: '' })
const bca: IBaseClassAttributes = BaseClass.read('a') as IBaseClassAttributes
console.log(bc.attributes.foo)
console.log(bca.foo)

// ExtendingClass
// Note that the create and read methods do not have to be overriden,
//  but typechecking still works as expected here
const ec: ExtendingClass = ExtendingClass.create({ foo: 'bar', bar: 0 })
const eca: IExtendingClassAttributes = ExtendingClass.read(
  'a'
) as IExtendingClassAttributes
console.log(ec.attributes.foo)
console.log(ec.attributes.bar)
console.log(eca.foo)
console.log(eca.bar)

Vous pouvez facilement étendre ce modèle aux actions CRUD restantes.

Dans l'exemple de @Jack-Barry, j'essaie d'obtenir la fonction read pour renvoyer une instance de la classe. Je m'attendais à ce que les éléments suivants fonctionnent:

class BaseClass<A extends IBaseClassAttributes = IBaseClassAttributes> {

  public static read<A extends IBaseClassAttributes, T extends BaseClass<A>>(
    this: ThisClass<A, T>,
    id: string,
  ): T | null {
    // Perform db retrieval here assign to variable
    const dbReturnValue = {} as A | null
    if (dbReturnValue === null) {
      return null;
    }
    return this.create(dbReturnValue);
  }
}

mais à la place, obtenez l'erreur Property 'create' does not exist on type 'ThisClass<A, T>'. Quelqu'un aurait-il une solution de contournement pour résoudre ce problème ?

@everhardt Étant donné que la méthode create est static , elle devrait être appelée sur le class de this , pas sur l'instance. Cela dit, je n'ai pas vraiment de bonne solution de contournement pour savoir comment accéder dynamiquement à la fonction class qui fournit toujours la vérification de type souhaitée.

Le plus proche que je puisse obtenir n'est pas particulièrement élégant et ne peut pas utiliser les type existants de BaseClass.create , ils pourraient donc facilement se désynchroniser :

return (this.constructor as unknown as { create: (attributes: A) => T }).create(dbReturnValue)

Je n'ai _ pas _ testé cela.

@Jack-Barry alors vous obtenez this.constructor.create is not a function .

Cela a du sens : Typescript interprète this comme l'instance, mais pour l'analyseur javascript éventuel, this est la classe car la fonction est statique.

Peut-être pas l'extension la plus inspirée de l'exemple de Jack-Barry, mais sur ce Playground Link , vous pouvez voir comment je l'ai résolu.

Le noeud:

  • le type ThisClass devrait décrire toutes les propriétés et méthodes statiques que les méthodes de BaseClass (à la fois statiques et sur l'instance) veulent utiliser avec un 'this' polymorphe.
  • lors de l'utilisation d'une propriété ou d'une méthode statique dans une méthode d'instance, utilisez (this.constructor as ThisClass<A, this>)

C'est un double travail, car vous devez définir les types de méthodes et de propriétés statiques à la fois sur la classe et sur le type ThisClass , mais pour moi, cela fonctionne.

edit : correction du lien du terrain de jeu

PHP a résolu ce problème il y a longtemps. soi. statique. cette.

Salut à tous, nous sommes en 2020, nous avons _ce_ problème. 😛

class Animal { 
  static create() { 
    return new this()
  }
}
class Bunny extends Animal {}
const bugs = Bunny.create() // const bugs: Animal

Je m'attendrais à ce que bugs soit une instance de Bunny .

Quelle est la solution de contournement actuelle ? J'ai tout essayé dans ce problème, rien ne semble s'arranger.

Mise à jour : Cela a fonctionné, a manqué la définition de l'argument this .

class Animal { 
  static create<T extends typeof Animal>(this: T): InstanceType<T> { 
    return (new this()) as InstanceType<T>
  }
}
class Bunny extends Animal {}
const bugs = Bunny.create() // const bugs: Bunny

Le problème avec cela est que les getters et les setters ne peuvent pas avoir de paramètres de type.

C'est aussi trop verbeux.

Je suis confronté à un problème similaire. Pour implémenter une "classe interne polymorphe", le mieux que j'ai pu trouver est le suivant :

class BaseClass {
  static InnerClass = class BaseInnerClass {};

  static createInnerClass<T extends typeof BaseClass>(this: T) {
    return new this.InnerClass() as InstanceType<T['InnerClass']>;
  }
}

class SubClass extends BaseClass {
  static InnerClass = class SubInnerClass extends BaseClass.InnerClass {};
}

const baseInnerClass = BaseClass.createInnerClass(); // => BaseInnerClass

const subInnerClass = SubClass.createInnerClass(); // => SubInnerClass

Cela semble bien fonctionner, mais la saisie de createInnerClass() est beaucoup trop verbeuse à mon avis, et j'en aurai beaucoup dans ma base de code. Quelqu'un at-il des idées sur la façon de simplifier ce code?

Le problème avec cette approche est qu'elle ne fonctionne pas avec les getters et les setters statiques.

Que diriez-vous d'implémenter self. pour les propriétés statiques ? J'ai eu ce problème il y a 4 ans, et je reviens sur le même sujet avec le même problème.

@sudomaxime Cela sort du cadre de ce problème et va à l'encontre des objectifs de conception de TypeScript tant qu'ECMAScript ne prend pas nativement en charge self. comme alias pour this.constructor. dans les classes, ce qui est peu probable arriver étant donné que self est un nom de variable commun.

La solution de @ abdullah-kasim semble fonctionner comme décrit, mais je ne parviens pas à la faire fonctionner avec des classes génériques :

class Animal<A> {
  public thing?: A;

  static create<T extends typeof Animal>(this: T): InstanceType<T> {
    return new this() as InstanceType<T>;
  }
}

type Foo = {};
class Bunny extends Animal<Foo> {}

const bunny = Bunny.create(); // typeof bunny is Animal<unknown>
bunny.thing;                  // unknown :(

   __     __
  /_/|   |\_\  
   |U|___|U|
   |       |
   | ,   , |
  (  = Y =  )
   |   `  |
  /|       |\
  \| |   | |/
 (_|_|___|_|_)
   '"'   '"'

J'apprécierais toute réflexion sur la faisabilité de cela, car je l'ai un peu bricolé sans succès.

@stevehanson Cela fonctionnerait-il pour vous ?

class Animal<A> {
  public thing?: A;

  static create<T>(this: new () => T): T {
    return new this() as T;
  }
}

type Foo = {asd: 123};
class Bunny extends Animal<Foo> {
    public hiya: string = "hi there"
}

const bunny = Bunny.create()


bunny.thing

const test = bunny.thing?.asd

const hiya = bunny.hiya

Testé cela sur une aire de jeux dactylographiée.

Inspiré de ce lien :
https://selleo.com/til/posts/gll9bsvjcj-generic-with-class-as-argument-and-returning-instance

Et je ne sais pas comment cela a réussi à impliquer que T est la classe elle-même . EDIT : Ah, je me suis souvenu, il a réussi à impliquer T parce que le paramètre silencieux this passera toujours la classe elle-même.

@abdullah-kasim cela a fonctionné ! Wow merci! Pour mon cas spécifique, mon constructeur prend un argument, donc c'était à quoi cela ressemblait pour moi, au cas où cela aiderait quelqu'un d'autre:

  static define<C, T, F = any, I = any>(
    this: new (generator: GeneratorFn<T, F, I>) => C,
    generator: GeneratorFn<T, F, I>,
  ): C {
    return new this(generator);
  }

Certaines des solutions ici ne fonctionnent pas pour les getters statiques car je ne peux passer aucun argument dans:

Étant donné l'exemple ci-dessous, comment puis-je déplacer le getter statique default vers une classe parent ?

class Letters {
  alpha: string = 'alpha'
  beta?: string
  gamma?: string
  static get default () {
    return new this()
  }
}

const x = Letters.default.alpha;

Cela peut être quelque chose à considérer si jamais nous envisageons des améliorations de syntaxe.

Peut-être lié : j'ai du mal à ajouter une autre couche d'abstraction à l'exemple de @abdullah-kasim. Essentiellement, j'aimerais pouvoir transformer Animal en une interface et permettre Bunny de définir son propre create() statique.

Voici un exemple pratique (pardonnez-moi d'abandonner l'analogie Bunny !). Je veux pouvoir définir quelques types de documents différents, et chacun devrait pouvoir définir une méthode de fabrique statique qui transforme une chaîne en une instance de document. Je veux faire respecter qu'un type de document ne doit définir un analyseur que pour lui-même, et non pour d'autres types de documents.

(les types de l'exemple ci-dessous ne sont pas tout à fait corrects - j'espère que ce que j'essaie d'accomplir est suffisamment clair)

interface ParseableDoc {
    parse<T>(this: new () => T, serialized:string): T|null;
}

interface Doc {
    getMetadata():string;
}

// EXPECT: no error!
const MarkdownDoc:ParseableDoc = class implements Doc {
    constructor(private meta:string){ };
    getMetadata():string { return this.meta; };

    static parse(serialized:string):typeof MarkdownDoc | null {
        // do something specific
        return null;
    }
}

// EXPECT: type error, since class defines no static parse()
const MissingParseDoc:ParseableDoc = class implements Doc {
    constructor(private meta:string){ };
    getMetadata():string { return this.meta; };
}

// EXPECT: type error, since parse() should return a MismatchedDoc
const MismatchDoc: ParseableDoc = class implements Doc {
    constructor(private meta:string){ };
    getMetadata():string { return this.meta; };

    static parse(serialized:string):typeof MismatchDoc | null {
        // do something specific
        return null;
    }
}

(Je pense) J'aimerais pouvoir écrire quelque chose comme ça :

interface ParseableDoc {
    getMetadata():string;
    static parse<T extends ParseableDoc>(this: new () => T, serialized:string): T|null;
}

class MarkdownDoc implements ParseableDoc {
    getMetadata():string { return ""; }

    static parse(serialized:string):MarkdownDoc|null {
        // do something specific
        return null;
    }
}

Une idée si une solution de contournement est possible ici aussi?

EDIT : solution de contournement possible

Un autre cas d'utilisation sur StackOverflow
Pour concevoir une classe de table de recherche générique.

// Generic part
abstract class Table<T extends Model> {
    instances: Map<number, T> = new Map();
}
abstract class Model {
    constructor(
        public readonly id: number,
        public table: Table<this>  // Error
    ) { 
        table.instances.set(id, this);
    }
}

// Example: a table of Person objects
class Person extends Model {
    constructor(
        id: number,
        table: Table<this>,  // Error
        public name: string
    ) {
        super(id, table);
    }
}
class PersonTable extends Table<Person> {}

const personTable = new PersonTable();
const person = new Person(0, personTable, 'John Doe');

// Note: the idea of using `this` as generic type argument is to guarantee
// that other models cannot be added to a table of persons, e.g. this would fail:
//     class SomeModel extends Model { prop = 0; }
//     const someModel = new SomeModel(1, person.table);
//                                        ^^^^^^^^^^^^

@ Think7 a commenté le 29 mai 2016
😰 Je ne peux pas croire que ce n'est toujours pas corrigé / ajouté .....

ha ha
2020 est là !

C'est ridicule et insensé. Nous sommes en 2020. 4 ans plus tard, pourquoi cela n'a-t-il pas été corrigé ?

Pourquoi ne pas implémenter quelque chose comme

class Model {
  static create(){
     return new static()
  }
  //or
  static create(): this {
     return new this()
  }
}

class User extends Model {
  //...
}

let user = new User.create() // type === User
Cette page vous a été utile?
0 / 5 - 0 notes