Typescript: Polimórfico "this" para membros estáticos

Criado em 1 dez. 2015  ·  150Comentários  ·  Fonte: microsoft/TypeScript

Ao tentar implementar um sistema de modelo de estilo de registro ativo bastante básico, mas polimórfico, encontramos problemas com o sistema de tipos que não respeita this quando usado em conjunto com um construtor ou modelo/genérico.

Já postei antes sobre isso aqui, #5493, e #5492 parece mencionar esse comportamento também.

E aqui está um post SO que eu fiz:
http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved

Reciclei meu exemplo de #5493 neste ticket para uma discussão mais aprofundada. Eu queria um bilhete aberto representando o desejo de tal coisa e de discussão, mas os outros dois estão fechados.

Aqui está um exemplo que descreve um modelo Factory que produz modelos. Se você quiser personalizar o BaseModel que volta do Factory você deve poder substituí-lo. No entanto, isso falha porque this não pode ser usado em um membro estático.

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

Talvez esse problema seja bobo e haja uma solução fácil. Congratulo-me com comentários sobre como esse padrão pode ser alcançado.

In Discussion Suggestion

Comentários muito úteis

Existe uma razão para este problema ter sido encerrado?

O fato de polimórfico não funcionar em estática basicamente torna esse recurso DOA, na minha opinião. Até agora, nunca precisei polimórficos disso em membros de instância, mas precisei a cada poucas semanas em estática, já que o sistema de manipulação de estática foi finalizado nos primeiros dias. Fiquei muito feliz quando esse recurso foi anunciado e, posteriormente, decepcionado ao perceber que só funciona em membros da instância.

O caso de uso é muito básico e extremamente comum. Considere um método de fábrica simples:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

Neste ponto, estou recorrendo a um casting feio ou desbaratando métodos static create() em cada herdeiro. Não ter esse recurso parece um buraco de completude bastante grande na linguagem.

Todos 150 comentários

O tamanho e a forma do meu barco são bastante semelhantes! Ah!

Um método de fábrica em uma superclasse retorna novas instâncias de suas subclasses. A funcionalidade do meu código funciona, mas exige que eu converta o tipo de retorno:

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.

Também me deparei com esse problema hoje!

Uma solução de correção é passar o tipo filho como um genérico estendendo a classe base, que é uma solução que aplico por enquanto:

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

Existe uma razão para este problema ter sido encerrado?

O fato de polimórfico não funcionar em estática basicamente torna esse recurso DOA, na minha opinião. Até agora, nunca precisei polimórficos disso em membros de instância, mas precisei a cada poucas semanas em estática, já que o sistema de manipulação de estática foi finalizado nos primeiros dias. Fiquei muito feliz quando esse recurso foi anunciado e, posteriormente, decepcionado ao perceber que só funciona em membros da instância.

O caso de uso é muito básico e extremamente comum. Considere um método de fábrica simples:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

Neste ponto, estou recorrendo a um casting feio ou desbaratando métodos static create() em cada herdeiro. Não ter esse recurso parece um buraco de completude bastante grande na linguagem.

@paul-go a questão não está encerrada... ?

@paul-go Também fiquei frustrado com esse problema, mas o abaixo é a solução alternativa mais confiável que encontrei. Cada subclasse Animal precisaria chamar super.create() e apenas converter o resultado em seu tipo. Não é grande coisa e é um forro que pode ser facilmente removido com isso adicionado.

O compilador, o intellisense e, o mais importante, o coelho estão todos felizes.

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 Oops ... por algum motivo eu confundi isso com # 5862 ... desculpe pela agressão do machado de batalha :-)

@ Think7 Sim ... daí o "recurso a métodos feios de conversão ou de lixo estático create () em cada herdeiro". É muito difícil quando você é um desenvolvedor de biblioteca e não pode realmente forçar os usuários finais a implementar um monte de métodos estáticos tipados nas classes que eles herdam de você.

legal. Totalmente perdido tudo sob o seu código :D

Meh valeu a pena, Tenho que desenhar um coelho.

:+1: coelho

:coelho: :coração:

+1, definitivamente gostaria de ver isso

Houve alguma atualização de discussão sobre este tópico?

Permanece em nosso enorme backlog de sugestões.

O Javascript já atua corretamente nesse padrão. Se o TS pudesse seguir também, isso nos salvaria de muito código clichê/extra. O "padrão de modelo" é bastante padrão, espero que o TS funcione como o JS faz nisso.

Eu também gostaria muito desse recurso pelos mesmos motivos de "Modelo CRUD" que todos os outros. Eu preciso disso em métodos estáticos mais do que métodos de instância.

Isso forneceria uma solução perfeita para o problema descrito em #8164.

É bom que existam "soluções" com substituições e genéricos, mas eles não estão realmente resolvendo nada aqui - todo o objetivo de ter esse recurso é evitar tais substituições/cast e criar consistência com o retorno de this type é tratado em métodos de instância.

Estou trabalhando nas tipagens para o Sequelize 4.0 e ele usa uma abordagem onde você subclassifica uma classe Model . Essa classe Model tem inúmeros métodos estáticos como findById() etc. que obviamente não retornam um Model mas sua subclasse, também conhecida this no contexto estático:

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

Isso não é possível digitar atualmente. Sequelize é _the_ ORM para Node e é triste que não possa ser digitado. Realmente precisa desse recurso. A única maneira é lançar toda vez que você chamar uma dessas funções ou substituir cada uma delas, adaptar o tipo de retorno e não fazer nada além de chamar super.method() .

Também um pouco relacionado é que os membros estáticos não podem fazer referência a argumentos de tipo genérico - alguns dos métodos usam um literal de objeto de atributos para o modelo que pode ser digitado por meio de um argumento de tipo, mas estão disponíveis apenas para membros de instância.

😰 Não acredito que isso ainda não foi corrigido/adicionado.....

Podemos fazer um bom uso disso:

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

declare class UIButton extends NSObject {
}

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

aqui está um caso de uso que eu gostaria que funcionasse (migrado de https://github.com/Microsoft/TypeScript/issues/9775 que eu fechei em favor disso)

Atualmente os parâmetros de um construtor não podem usar o tipo this neles:

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

Esperado: isso estará disponível para parâmetros do construtor.

Um problema que se pretende resolver:

  • ser capaz de basear minha API fluente em torno de si mesma ou em torno de outra API fluente reutilizando o mesmo código:
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();

Solução à prova de futuro:

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

Dessa forma, quando o TypeScript suportar this para membros estáticos, o novo código de usuário será Baz.create() em vez de Baz.create<Baz>() , enquanto o código de usuário antigo funcionará bem. :sorriso:

Isso é realmente necessário! especialmente para DAO que tem métodos estáticos retornando a instância. A maioria deles definidos em um DAO base, como (salvar, obter, atualizar etc...)

Posso dizer que isso pode causar alguma confusão, tendo o tipo this em um método estático resolvido para a classe em que está e não para o tipo da classe (ou seja: typeof ).

No JS real, chamar um método estático em um tipo resultará em this dentro da função sendo a classe e não uma instância dela...

No entanto, na intuição das pessoas, acho que a primeira coisa que aparece ao ver o tipo de retorno this em um método estático é o tipo de instância ...

@shlomiassaf Não seria inconsistente. Quando você especifica uma classe como um tipo de retorno para uma função como User, o tipo de retorno será uma instância do usuário. Exatamente o mesmo, quando você define um tipo de retorno this em um método estático o tipo de retorno será uma instância disto (uma instância da classe). Um método que retorna a própria classe pode então ser modelado com typeof this.

@felixfbecker isso é totalmente uma coisa de ponto de vista, é assim que você escolhe olhar para isso.

Vamos inspecionar o que acontece no JS, para que possamos inferir a lógica:

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

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

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

Agora, this em tempo de execução real é o contexto limitado da função, isso é exatamente o que acontece em interfaces tipo api fluente e o polimórfico esse recurso ajuda retornando o tipo certo, que está apenas se alinhando com o funcionamento do JS, Uma classe base retornando this retorna a instância criada pela classe derivada. O recurso é na verdade uma correção.

Resumindo:
Espera-se que um método retornando this que é definido como parte do protótipo (instância) retorne a instância.

Continuando com essa lógica, espera-se que um método retornando this que é definido como parte do tipo de classe (protótipo) retorne o contexto limitado, que é o tipo de classe.

Novamente, sem preconceito, sem opinião, fatos simples.

Em termos de intuição, me sentirei confortável tendo this retornado de uma função estática representando a instância, pois ela está definida dentro do tipo, mas isso sou eu. Outros podem pensar de forma diferente e não podemos dizer-lhes que estão errados.

O problema é que precisa ser possível digitar tanto um método estático que retorna uma instância de classe quanto um método estático que retorna a própria classe (typeof this). Sua lógica faz sentido do ponto de vista do JS, mas estamos falando de return _types_ aqui, e usar uma classe como um tipo de retorno (aqui isto) no TypeScript e em qualquer outra linguagem sempre significa a instância da classe. Para retornar a classe real, temos o operador typeof.

@felixfbecker Isso levanta outra questão!

Se o tipo this for o tipo de instância, será diferente do que a palavra-chave this se refere no corpo do método estático. return this produz um tipo de retorno de função typeof this , o que é totalmente estranho!

Não, não é. Quando você define um método como

getUser(): User {
  ...
}

você espera obter um User _instance_ de volta, não a classe User (é para isso que serve typeof ). É assim que funciona em todas as linguagens digitadas. A semântica de tipo é simplesmente diferente da semântica de tempo de execução.

Por que não usar as palavras-chave this ou static como um construtor em função da manipulação com uma classe filha?

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

E se compararmos isso com um exemplo em JS, veremos o que funciona corretamente:

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

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

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

Você não pode usar o tipo this em um método estático, acho que essa é a raiz do problema.

@felixfbecker

Considere isto:

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

Essa anotação de tipo é intuitiva, mas incorreta se o tipo this em um método estático for a instância da classe. A palavra-chave this tem um tipo de typeof this em um método estático!

Eu sinto que this _should_ se refere ao tipo de instância, não ao tipo de classe, porque podemos obter uma referência ao tipo de classe do tipo de instância ( typeof X ), mas não vice-versa ( instancetypeof X ?)

@xealot ok, por que não usar a palavra-chave static em vez this ? Mas this no contexto estático JS ainda aponta para um construtor.

@izatop sim, o javascript gerado funciona (corretamente ou não). No entanto, isso não é sobre Javascript. A reclamação não é que a linguagem Javascript não permite esse padrão, é que o sistema de tipos do Typescript não permite.

Você não pode manter a segurança de tipo com esse padrão porque o Typescript não permite tipos polimórficos this em membros estáticos. Isso inclui métodos de classe definidos com a palavra-chave static e constructor .

Link do Playground

@LPGhatguy , mas você leu meu comentário anterior, certo?! Se você tiver um método com tipo de retorno User , você espera return new User(); , não return User; . Da mesma forma que um valor de retorno de this deve significar return new this(); em um método estático. Em métodos não estáticos isso é diferente, mas é claro porque this é sempre um objeto, você não poderia instanciá-lo. Mas no final, basicamente todos concordamos que essa é a melhor sintaxe por causa de typeof e que esse recurso é muito necessário no TS.

@xealot eu entendo o que o TypeScript não permite polimórfico this , no entanto, vou perguntar por que não adicionar esse recurso ao TS?

Não sei se o seguinte funcionará para todos os casos de uso deste problema, mas descobri que usar o tipo this: em métodos estáticos em conjunto com o uso inteligente do sistema de inferência de tipos permite diversão digitando. Pode não ser incrivelmente legível, mas faz o trabalho sem ter que redefinir métodos estáticos nas classes filhas.

Funciona com [email protected]

Considere o seguinte ;

// 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 por que this é dado como parâmetro?

Isso se deve a https://github.com/Microsoft/TypeScript/issues/3694 que foi enviado com o typescript 2.0.0 que permite definir este parâmetro para funções (permitindo assim o uso de this em seu corpo sem problemas e também para evitar o uso incorreto das funções)

Acontece que podemos usá-los em métodos e métodos estáticos, e isso permite coisas realmente engraçadas, como usá-lo para "ajudar" o inferidor de tipos, forçando-o a comparar o this que fornecemos (IModelo) ao que está sendo usado (Cão ou Gato). Ele então "adivinhar" o tipo correto para T e o usa, digitando corretamente nossa função.

(Observe que this é um parâmetro _special_ entendido pelo compilador typescript, ele não adiciona mais um à função)

Eu pensei que a sintaxe era algo como <this extends Whatever> . Então isso ainda funcionaria se create() tiver outros parâmetros?

Absolutamente

Eu também queria salientar que isso é importante para tipos internos.
Quando você subclasse Array por exemplo, o método from() da subclasse retornará uma instância da subclasse, não 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

Alguma atualização sobre este assunto? Esse recurso melhoraria muito a experiência da minha equipe com o TypeScript.

Observe que isso é bastante crítico, pois é um padrão comumente usado em ORMs e muitos códigos se comportam de maneira muito diferente do que o compilador e o sistema de tipos acham que deveria.

Eu levantaria alguns contra-argumentos contra os contra-argumentos.

Como @shlomiassaf apontou, permitir this em membros estáticos causará confusão porque this significa coisas diferentes no método estático e no método de instância. Além disso, this na anotação de tipo e this no corpo do método têm semânticas diferentes. Podemos evitar essa confusão introduzindo uma nova palavra-chave, mas o custo de uma nova palavra-chave é alto.

E há uma solução fácil no TypeScript atual. @ceymardmostrou isso . E eu preferiria sua abordagem porque captura a semântica de tempo de execução do método estático. Considere este código.

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

Se polimórfico for suportado, o compilador deve relatar um erro em class B extends A . Mas esta é uma mudança de ruptura.

@HerringtonDarkholme Não vejo a confusão, o tipo de retorno de this é exatamente o que eu esperaria de return new this() . Potencialmente, poderíamos usar self para isso, mas introduzir outra palavra-chave (diferente da usada para produzir a saída) parece ser mais confuso para mim. Já o usamos como tipo de retorno, o único (mas grande) problema é que ele não é suportado em membros estáticos.

O compilador não deve exigir que os programadores façam soluções alternativas, o TypeScript deve ser um "JavaScript aprimorado" e isso não é verdade neste caso, você precisa fazer uma solução complexa para que seu código não gere milhões de erros. É bom que possamos alcançá-lo na versão atual de alguma forma, mas isso não significa que está tudo bem e não precisa de uma correção.

Por que o tipo de retorno de this é diferente do tipo de this no corpo do método, então?
Por que esse código falha? Como você pode explicar isso para um recém-chegado?

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

Por que um superconjunto de JavaScript não aceitaria isso?

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

abstract class B extends A {}

É verdade que talvez precisemos introduzir outra palavra-chave então. Pode ser self , ou talvez instance

@HerringtonDarkholme Aqui está como eu explico isso para um recém-chegado.
Quando você faz

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

o tipo de retorno de B significa que você precisa retornar uma instância de B . Se você quiser retornar a classe em si, você precisa usar

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

Agora, assim como em JavaScript, this em membros estáticos se refere à classe. Então, se você usar um tipo de retorno this em um método estático, você terá que retornar uma instância da classe:

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

se você quiser retornar a classe em si, você precisa usar typeof this :

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

é exatamente assim que os tipos já funcionam no TS desde sempre. Se você digitar com uma classe, o TS espera uma instância. Se você quiser digitar para o próprio tipo de classe, precisará usar typeof .

Então eu perguntaria porque this não tem o mesmo significado no corpo do método. Se this na anotação de tipo do método de instância tem o mesmo significado que this no corpo do método correspondente, por que não é o mesmo para o método estático?

Estamos diante de um dilema e há três opções:

  1. make this significa coisas diferentes em contextos diferentes (confusão)
  2. introduzir uma nova palavra-chave como self ou static
  3. deixe o programador escrever uma anotação de tipo mais estranha (impactar alguns usuários)

Eu prefiro a terceira abordagem porque reflete a semântica de tempo de execução da definição de método estático .
Ele também detecta erros no site de uso, em vez de definir site, ou seja, como neste comentário , o erro é relatado em B.create , não class B extends A . Use o erro do site é mais preciso neste caso. (considere que você declara um método estático que faz referência a this e então declara uma subclasse abstrata).

E, o mais importante, não exige uma proposta de linguagem para o novo recurso.

Na verdade, a preferência difere das pessoas. Mas pelo menos eu quero ver uma proposta mais detalhada como essa . Existem muitos problemas a serem resolvidos, como designabilidade de classes abstratas, assinaturas de construtores incompatíveis de subclasses e estratégias de relatório de erros.

@HerringtonDarkholme Porque no caso de métodos de instância, o tempo de execução this é uma instância de objeto e não uma classe, portanto, não há espaço para confusão. É claro que o tipo this é literalmente o runtime this , não há outra opção. Enquanto no caso estático ela se comporta como qualquer outra anotação de classe em qualquer linguagem de programação orientada a objetos: anote-a com uma classe e você espera uma instância de volta. Além disso, no caso do TS, porque em JavaScript as classes também são objetos, digite-o com typeof uma classe e você receberá a classe literal de volta.

Quero dizer, provavelmente teria sido mais consistente quando eles implementaram o tipo de retorno this para também pensar no caso estático e exigir que os usuários sempre escrevessem typeof this para métodos de instância. Mas essa decisão foi tomada naquela época, então temos que trabalhar com isso agora, e como eu disse, não deixa espaço para confusão porque this em métodos de instância não produz nenhum outro tipo (ao contrário de classes, que tem um tipo estático e um tipo de instância), então é distinto nesse caso e não vai confundir ninguém.

OK, vamos supor que ninguém ficará confuso com this . Como sobre a atribuição de classe abstrata?

A estática dos métodos é um tanto arbitrária. Qualquer função pode ter propriedades e métodos. Se também puder ser chamado com new , escolhemos chamar essas propriedades e métodos static . Agora, esta convenção foi firmemente cimentada em ECMAScript.

@HerringtonDarkholme você está sem dúvida correto que o uso de this causaria confusão. No entanto, como não há nada de errado em usar this , eu diria que está perfeitamente bem, especialmente levando em consideração sua astuta análise de custo-benefício das várias alternativas. Acho que this é a alternativa menos ruim.

Acabei de tentar acompanhar este tópico do meu post original, sinto que talvez isso tenha saído um pouco dos trilhos.

Para esclarecer, o desejo é que a anotação do tipo this seja polimórfica quando usada no construtor, especificamente como um genérico/modelo. Com relação a @shlomiassaf e @HerringtonDarkholme , acredito que o exemplo com métodos static pode ser confuso e não é a intenção deste problema.

Embora nem sempre seja considerado como tal, o construtor de uma classe é estático. O exemplo (que vou repassar com comentários mais esclarecedores) não está declarando this em um método estático, mas sim declarando this para uso futuro por meio de um genérico na anotação de tipo de um método estático .

A diferença é que eu não quero this computado imediatamente em um método estático, mas no futuro em um método dinâmico.

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

Para quem acha que isso é confuso, bem, não é super direto, eu concordo. No entanto, o uso de this no construtor BaseModel não é realmente um uso estático, é um uso adiado a ser calculado posteriormente. No entanto, métodos estáticos (incluindo o construtor) não funcionam dessa maneira.

Eu acho que em tempo de execução tudo isso funciona como esperado. No entanto, o sistema de tipos é incapaz de modelar esse padrão específico. A incapacidade do typescript de modelar um padrão javascript é a base do motivo pelo qual esse problema está em aberto.

Desculpe se este é um comentário confuso, escrito às pressas.

@xealot Entendo seu ponto, mas outros problemas que sugeriram especificamente this polimórfico para métodos estáticos foram fechados como uma duplicata deste. Suponho que a correção no TS habilitaria os dois casos de uso.

Qual é o seu caso de uso exato? Talvez a solução 'isto' seja suficiente.

Eu o uso em uma biblioteca ORM personalizada com sucesso
Le jeu. 20 out. 2016 às 19:15, Tom Marius [email protected] a
escrito:

Observe que isso é bastante crítico, pois é um método comumente usado
padrão em ORMs e muito código se comporta de maneira muito diferente do
compilador e sistema de tipos acham que deveria.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -255169194,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AAtAoRHc45B386xdNhuCLB8iW6i82e7Uks5q16GzgaJpZM4GsmOH
.

Obrigado a @ceymard e @HerringtonDarkholme.

static create<T extends A>(this: {new (): T}) { return new this(); } fez o truque para mim 👍

É mais obscuro que static create(): this { return new this(); } à primeira vista, mas pelo menos está correto! Ele captura com precisão o tipo de this dentro do método estático.

Espero que isso de alguma forma possa ser priorizado, mesmo que não seja o mais votado ou comentado etc. É muito chato ter que usar o argumento de tipo para obter o tipo certo, por exemplo:

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

Quando é praticamente possível fazer apenas:

let u = User.create();

E é inevitável que static às vezes se refira a tipos de instância.

Eu tive outra ideia sobre a discussão sobre se o tipo this em membros estáticos deveria ser o tipo do lado estático da classe ou o lado da instância. this poderia ser o lado estático, corresponderia ao que é feito nos membros da instância e qual é o comportamento do tempo de execução, e então você poderia obter o tipo de instância por meio de this.prototype :

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

class User extends Model {}

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

isso estaria de acordo com a forma como funciona em JavaScript. this é o próprio objeto de classe e a instância está em this.prototype . É essencialmente a mesma mecânica dos tipos mapeados, equivalente a this['prototype'] .

Da mesma forma, você pode imaginar o lado estático disponível nos membros da instância por meio this.constructor (não consigo pensar em um caso de uso para este caixa eletrônico).

Eu também queria mencionar que nenhuma das soluções/hacks mencionadas funciona para digitar o modelo ORM no Sequelize.

  • A anotação de tipo this não pode ser usada em um construtor, portanto não funciona para esta assinatura:
    ts abstract class Model { constructor(values: Partial<this.prototype>) }
  • Passar o tipo como um argumento de classe genérico não funciona para métodos estáticos, pois membros estáticos não podem referenciar parâmetros genéricos
  • Passar o tipo como um argumento de método genérico funciona para métodos, mas não funciona para propriedades estáticas, como:
    ts abstract class Model { static attributes: { [K in keyof this.prototype]: { type: DataType, field: string, unique: boolean, primaryKey: boolean, autoIncrement: boolean } }; }

+1 para esta solicitação. meu ORM será muito limpo.

Espero que veremos a solução limpa em breve

Enquanto isso, obrigado a todos que ofereceram soluções!

Podemos avançar nisso, já que não há mais discussão?

Onde você gostaria de levá-lo? Esta ainda é uma maneira pela qual o Typescript não pode modelar Javascript de forma limpa, tanto quanto eu sei, e não vi uma solução além de simplesmente não fazê-lo.

Este é um recurso obrigatório para desenvolvedores de ORM, pois já podemos ver que três proprietários de ORM populares solicitaram esse recurso.

@pleerock @xealot As soluções foram propostas acima:

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

Estou usando isso extensivamente. As declarações dos métodos estáticos nas classes base são um pouco pesadas, mas esse é o preço a pagar para evitar distorções no tipo de this dentro dos métodos estáticos.

@pleerock

Eu tenho um ORM interno que usa o padrão this: extensivamente sem problemas.

Acho que não há necessidade de sobrecarregar o idioma quando o recurso já está aqui, embora reconhecidamente um pouco complicado. O caso de uso para uma sintaxe mais clara é bastante limitado e pode apresentar inconsistências.

Talvez possa haver um thread Stackoverflow com esta pergunta e soluções para referência?

@bjouhier @ceymard Expliquei por que todas as soluções alternativas neste tópico não funcionam para o Sequelize: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -269463313

E não se trata apenas de ORMs, mas também da biblioteca padrão: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -244550725

@felixfbecker Acabei de postar algo sobre o caso de uso do construtor e o excluí (percebi que o TS não exige construtores em todas as subclasses logo após a postagem).

@felixbecker Em relação ao caso do construtor, eu o resolvo aderindo a construtores sem parâmetros e fornecendo métodos estáticos especializados (criar, clonar, ...). Mais explícito e fácil de digitar.

@bjouhier Isso significaria que você teria que redeclarar basicamente todos os métodos em todas as subclasses do modelo. Olha quantos tem no Sequelize: http://docs.sequelizejs.com/class/lib/model.js~Model.html

Meu argumento é de uma perspectiva completamente diferente. Embora existam soluções alternativas parciais e pouco intuitivas e possamos discutir e discordar se elas são suficientes ou não, podemos concordar que esta é uma área em que o Typescript não pode modelar facilmente o Javascript.

E meu entendimento é que o Typescript deve ser uma extensão do Javascript.

@felixfbecker

Isso significaria que você teria que redeclarar basicamente todos os métodos em cada subclasse de modelo.

Por quê? Essa não é a minha experiência. Você pode ilustrar o problema com um exemplo de código?

@bjouhier Eu ilustrei o problema em profundidade com exemplos de código em meus comentários neste tópico, para começar https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -222348054

Mas veja meu exemplo create acima . O método create é um método estático. Ele não é redeclarado e, no entanto, é digitado corretamente na subclasse. Por que os métodos do Model precisam de uma redefinição então?

@felixfbecker Seu exemplo _para começar_:

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 Ok, parece que as anotações this: { new (): T } realmente fazem a inferência de tipos funcionar. O que me faz pensar por que esse não é o tipo padrão que o compilador usa.
A solução alternativa obviamente não funciona para o construtor, pois eles não podem ter um parâmetro this .

Sim, não funciona para construtores, mas você pode contornar o problema com uma pequena alteração na API, adicionando um método estático create .

Eu entendo isso, mas esta é uma biblioteca JavaScript, então estamos falando sobre digitar a API existente em uma declaração.

Se this significasse { new (): T } dentro de um método estático, então this não seria o tipo correto para this dentro do método estático. Por exemplo new this() não seria permitido. Para consistência, this deve ser o tipo da função construtora, não o tipo da instância da classe.

Eu recebo o problema com a digitação de uma biblioteca existente. Se você não tiver controle sobre essa biblioteca, ainda poderá criar uma classe base intermediária ( BaseModel extends Model ) com uma função create corretamente digitada e derivar todos os seus modelos de BaseModel .

Se você quiser acessar as propriedades estáticas da classe base, você pode usar

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

Mas você provavelmente tem um ponto válido sobre o construtor. Não vejo uma razão difícil pela qual o compilador deve rejeitar o seguinte:

constructor(values: Partial<this>) {}

Concordo com @xealot que há dor aqui. Seria tão legal se pudéssemos escrever

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

em vez de

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

Mas precisamos de um novo operador no sistema de digitação ( instanceof ?). O seguinte é inválido:

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

O TS 2.4 quebrou essas soluções alternativas para mim:
https://travis-ci.org/types/sequelize/builds/247636686

@sandersn @felixfbecker Acho que esse é um bug válido. No entanto, não posso reproduzi-lo de forma mínima. O parâmetro de retorno de chamada é contravariante.

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

Alguma chance de isso ser corrigido em algum momento?

Passei por isso hoje também. Basicamente, eu queria construir uma classe base singleton, assim:

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

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

O uso seria algo como:

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

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

Eu tentei cerca de 10 variações disso, com a variante StaticThis mencionada acima e muitas outras. No final, há apenas uma versão que seria compilada, mas o resultado de getInstance() foi derivado como apenas Object .

Eu sinto que é muito mais difícil do que precisa ser trabalhar com construções como essas.

Seguintes trabalhos:

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

A parte this as any as typeof Singleton é feia, mas indica que estamos enganando o sistema de tipos, pois _instance deve ser armazenado no construtor da classe derivada.

Normalmente, a classe base será enterrada em sua estrutura, portanto, não prejudicará muito se sua implementação for um pouco feia. O que importa é código limpo em classes derivadas.

Eu estava esperando em 2.8 nightlies poder fazer algo assim:

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

Infelizmente não tive essa sorte :(

Os exemplos de código @bjouhier funcionam como um encanto. Mas quebre o preenchimento automático do IntelliSense.

O React 16.3.0 foi lançado e parece impossível digitar corretamente o novo método getDerivedStateFromProps , dado que um método estático não pode referenciar parâmetros de tipo de classe:

(exemplo simplificado)

class Component<P, S> {

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

    props: P
    state: S
}

Existe alguma solução alternativa? (Eu não conto "digitar P e S como PP e SS e espero que os desenvolvedores façam essas duas famílias de tipos corresponderem exatamente" como uma :p)

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

@cncolder Eu também testemunhei a quebra do intellisense. Eu acredito que começou com typescript 2.7.

Uma coisa que não vejo aqui, estendendo no exemplo Singleton - seria um padrão comum fazer uma classe Singleton / Multiton ter um construtor protegido - e isso não pode ser estendido se o tipo for { new(): T } como este impõe um construtor público - o termo this ou outra solução fornecida aqui ( instanceof this , typeof this etc) deve resolver esse problema. Isso é definitivamente possível com o lançamento de um erro no construtor se o singleton não solicitar sua criação, etc., mas isso anula o objetivo de erros de digitação - ter uma solução em tempo de execução ...

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

Eu não sei por que você precisaria de um singleton em TS com getInstance() , você pode apenas definir um literal de objeto. Ou se você realmente deseja usar classes (para membros privados), exporte apenas a instância de classe e não a classe, ou use uma expressão de classe ( export new class ... ).

Isso é mais útil para o Multiton - e acredito ter encontrado um padrão que funciona,... embora ainda seja uma sensação muito hacky sem esse tipo.

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

Eu também tenho algum código para armazenar a chave para que seja acessível na classe filha na criação, mas retirei isso por simplicidade ...

Oi. Estou encontrando um problema que precisa de "this" polimórfico para o construtor. Semelhante a este comentário.

Eu quero criar uma classe. A classe em si tem muitas propriedades, então o construtor não ficará bem se todas as propriedades dessa classe forem inicializadas por meio de parâmetros do construtor.

Eu prefiro um construtor que aceita um objeto para inicializar propriedades. Por exemplo:

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

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

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

Então, eu quero estender a classe. Por exemplo:

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

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

Será muito bom se o tipo this puder ser usado no construtor também.

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

Portanto, o IntelliSense pode inferir propriedades da classe filha sem clichê adicional: por exemplo, redefinindo o construtor. Parecerá mais natural do que usar um método estático para inicializar a classe.

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

Muito obrigado pela sua consideração.

Estou feliz por ter encontrado este tópico. Sinceramente, acho que esse é um problema muito sério e fiquei muito desencorajado em vê-lo passar para o 3.0.

Olhando para todas as soluções e considerando as várias soluções alternativas com as quais brinquei nos últimos dois anos, chego à conclusão de que qualquer solução rápida que tentarmos simplesmente falhará se e quando uma solução estiver em vigor. E até então, cada coerção que fazemos simplesmente requer muito trabalho manual para mantê-la em todo o projeto. E uma vez publicado, você não pode pedir aos consumidores que entendam sua própria mentalidade e por que você escolheu consertar algo que é TypeScript se eles simplesmente consumirem seu pacote em vanilla, mas como a maioria de nós, trabalhamos com VSCode.

Ao mesmo tempo, escrever código para deixar o Type System feliz quando a linguagem real não é o problema vai contra o mesmo motivo para usar os recursos Typed para começar.

Minha recomendação a todos é aceitar que os rabiscos estão com defeito e usar //@ts-ignore because my code works as expected quando possível.

Se o seu linter é imune à inteligência humana razoável, é hora de encontrar um que saiba o seu lugar.

ATUALIZAÇÃO :
Eu queria adicionar minha própria tentativa de aumentar a inferência estática com segurança . Essa abordagem é estritamente ambiental, pois se destina a estar fora do caminho. É exaustivamente explícito, forçando você a verificar seus aumentos e não apenas assumir que a herança fará o trabalho por você.

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

Obviamente, não posso garantir que esse padrão seja válido, nem que satisfaça todos os cenários deste segmento, mas, por enquanto, funciona conforme o esperado.

Essas escolhas decorrem da percepção preocupante de que até este ponto os próprios arquivos lib do TypeScript incluem apenas duas declarações de classe!

SafeArray

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

VarDateName

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

Discutido por um tempo hoje. Pontos chave:

  • this refere-se ao lado da instância ou ao lado estático?

    • Definitivamente o lado estático. O lado da instância pode ser referido via InstanceTypeOf mas o inverso não é verdadeiro. Isso também mantém a simetria de que this na assinatura tem o mesmo tipo que this no corpo.

  • Problemas de solidez

    • Não há garantias de que uma lista de parâmetros de construtor de classe derivada tenha alguma relação com sua lista de parâmetros de construtor de classe base

    • As classes quebram a substituibilidade do lado estático a esse respeito com muita frequência

    • A substituibilidade não do lado do construto é imposta e isso é tipicamente o que as pessoas estão interessadas de qualquer maneira

    • Já permitimos invocações incorretas de new this() em métodos static

    • Provavelmente ninguém se importa com o lado da assinatura de construção da estática this do lado de fora?

Soluções alternativas existentes:

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

Notavelmente, isso só funciona se a assinatura de construção Bar for adequadamente substituível por Foo

@RyanCavanaugh Estou tentando encontrar uma explicação em que o valor de this vem do seu exemplo static boo<T extends typeof Foo>(this: T): InstanceType<T> , mas não entendi. Para ter certeza, reli tudo sobre genéricos, mas ainda assim - não. Se eu substituir this por clazz por exemplo, não funcionará mais e o compilador reclamará com "Esperado 1 argumentos, mas obteve 0." Então alguma mágica está acontecendo lá. Você poderia explicar? Ou me aponte para a direção certa na documentação?

Editar:
Além disso, por que é extends typeof Foo e não simplesmente extends Foo ?

@creynders this é um parâmetro falso especial que pode ser digitado para o contexto da função. documentos .

Usar InstanceType<T> não funciona quando há genéricos envolvidos.

Meu caso de uso é mais ou menos assim:

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

Link do playground TypeScript

@mastermatt oh uau, totalmente NÃO entendi isso ao ler os documentos ... Obrigado

Olhando para o exemplo de @RyanCavanaugh , consegui fazer nossos métodos estáticos funcionarem, mas também tenho métodos de instância que se referem aos métodos estáticos, mas não consigo descobrir como obter a tipagem correta para isso.

Exemplo:

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

Se eu pudesse encontrar uma solução para este último problema, ficaria pronto até que algo oficial aparecesse.

Também digno de nota com esse exemplo, se a subclasse tentar substituir qualquer método estático e chamar super , ela também ficará chateada... Tirando esses dois problemas, a solução parece estar funcionando bem. Embora, esses dois problemas sejam bastante bloqueadores para o nosso código.

IIRC isso ocorre porque isso não é exprimível em texto datilografado sem dobrar um pouco o sistema de tipos. Como em ; this.constructor não tem garantia de ser o que você pensa que é ou algo assim.

Nesse caso preciso, eu teria o método de instância boo() retornando this e trapaceando um pouco, forçando o compilador a aceitar que eu sei o que estou fazendo.

Meu raciocínio geral é que eu quero que a API seja o mais simples possível para o resto do código, mesmo que às vezes isso signifique trapacear um pouco.

Se alguém tiver algo mais robusto, eu adoraria

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

Você também pode seguir a rota da interface e fazer algo assim;

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

Isso também é trapaça, mas talvez um pouco mais controlado? Eu realmente não posso dizer.

Então, o primeiro argumento this no método estático "boo" é tratado especialmente, não como o argumento regular? Algum link para documentos, descrevendo isso?

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

Estou com o mesmo problema (pelo menos parecido).
Estou usando Vanilla Javascript com JSDoc (não TypeScript), então não posso usar/implementar as soluções alternativas que giram em torno de genéricos propostos neste tópico, mas a raiz parece ser a mesma.
Eu abri este problema: #28880

Esta edição já tem literalmente 3 anos.
Alguém encontrou uma solução adequada que poderia funcionar para mim?

Três anos

O trabalho de @RyanCavanaugh é ótimo para métodos estáticos, mas e um acessador estático?

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

Três anos

Ok, você é meio decente em subtrair datas. Mas você pode oferecer uma solução? ;)

@RyanCavanaugh Isso é um problema quando usamos this em geradores da seguinte forma:

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

Quando fazemos geradores, não podemos usar funções de seta para usar this . Então agora temos que declarar o tipo this explicitamente nas subclasses. Um caso real é o seguinte:

      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

Não precisamos declarar o tipo this nas subclasses se pudermos fazê-lo nas classes base. No entanto, this não é inicializado quando o gerador é chamado de forma síncrona na superclasse. Evitei esse problema no código, mas esse é um hack sujo. Portanto, esse padrão pode originalmente ser incompatível com linguagens de programação baseadas em classes.

Não é o mais limpo, mas pode ser útil para acessar a classe filha de um método estático.

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

class C extends Base {
}

C.foo();

Embora eu não tenha certeza se o problema que estou enfrentando atualmente é devido a isso, mas depois de uma breve olhada nesse problema, parece que provavelmente é o caso. Por favor, corrija se não for o caso.

Então eu tenho as seguintes 2 classes relacionadas via herança.

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

Em seguida, adiciono um método mapObject #$1$#$ na classe SubTarget da seguinte forma.

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

Embora eu esperasse que isso funcionasse, recebo o seguinte erro.

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

Este erro é gerado devido a este problema? Caso contrário, uma explicação seria muito boa.

Link para o playground

Vim aqui enquanto procurava uma solução para anotar métodos estáticos que retornam novas instâncias da classe real que são chamadas.

Eu acho que a solução sugerida por @SMotaal (em combinação com new this() ) é a mais limpa e faz mais sentido para mim. Ele permite expressar a intenção desejada, não me obriga a especificar o tipo em cada chamada ao método genérico e não adiciona nenhuma sobrecarga no código final.

Mas falando sério, como isso ainda não faz parte do núcleo do Typescript? É cenário bastante comum.

@danielvy — Acho que a desconexão entre POO e protótipos é a maior lacuna mental que não deixa todos felizes. Eu acho que as principais suposições sobre classes feitas muito antes de os recursos de classe evoluírem na especificação não se alinharam, e romper com isso é uma armadilha para muitos recursos do TypeScript, então todos estão "priorizando" e tudo bem, mas não para " tipos" na minha opinião. Não ter uma solução melhor do que a concorrência só é um problema se houver concorrência, é por isso que todos os ovos em uma cesta são sempre ruins para todos – estou sendo pragmático e sincero aqui.

Isso não funciona:

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.

Mas isso não deveria ser permitido, já que a extensão estende a base?

Observe que isso funciona:

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

mas então você não pode usar this.member dentro dele.

Então, estou supondo da postagem do @ntucker que a solução alternativa funciona apenas em um nível de profundidade, a menos que a classe estendida corresponda exatamente à classe base?

Olá! E quanto aos construtores protegidos ?

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

  protected constructor() {}
}

class B extends A {} 

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

Se o construtor for público - tudo bem, mas se não, causará um erro:

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.

Playground - http://tiny.cc/r74c9y

Há alguma esperança para isso? Acabei de me deparar com essa necessidade mais uma vez :F

@ntucker Duuuuudee, você conseguiu!!! A principal realização para mim foi tornar o método de criação estático genérico, declarar this param e, em seguida, fazer a conversão InstanceType<U> no final.

Parque infantil

De cima ^

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 😄 

Isso satisfaz perfeitamente meu caso de uso e parece que, a partir do TS 3.5.1, isso também satisfaz a maioria das pessoas neste segmento (consulte playground para explorar o prop estático theAnswer , bem como o 3º nível de extensão)

Hot take: Acho que isso funciona agora e pode ser fechado?

@jonjaques Aliás, você nunca usa T. Além disso, isso não resolve o problema que descrevi, que é substituir métodos.

Sinto que a solução mencionada já foi sugerida há quase 4 anos… E mesmo assim não é algo que resolva o problema.

Abri um stackoverflow para ver se havia outras soluções alternativas que alguém poderia conhecer e alguém tinha um bom resumo do problema que estou enfrentando:

"Genéricos usados ​​como parâmetros de método, incluindo esses parâmetros, parecem ser contravariantes, mas na verdade você sempre quer que esses parâmetros sejam tratados como covariantes (ou talvez até bivariantes)."

Portanto, parece que isso é realmente algo que está quebrado no próprio TypeScript e deve ser corrigido ('this' deve ser covariante ou bivariante).

EDIT: Este comentário tem uma maneira melhor.

Não tenho muito a acrescentar, mas isso funcionou bem para mim em 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()

Espero que isso seja útil para qualquer pessoa que não queira navegar por esse longo problema.

Outro caso de uso são funções de membro de guarda de tipo personalizado em não classes:

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

A solução quase me leva aonde preciso estar, mas tenho mais uma chave para colocar nas engrenagens. Veja o seguinte exemplo inventado:

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

Aqui está o erro:

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)

Isso faz sentido para mim, pois o método create #$2$#$ na classe Automobile não tem referência a ITruckOptions até O , mas estou querendo saber se há um solução para esse problema?

Normalmente, as opções da minha classe de extensão para o construtor estenderão a interface de opções da classe base, então eu sei que eles sempre incluirão os parâmetros para a classe base, mas não tenho uma maneira confiável de garantir que eles incluam os parâmetros para a extensão classe.

Eu também tive que recorrer apenas a métodos de substituição na extensão de classes para informá-los sobre os tipos de entrada e retorno esperados da classe herdada que parecem um pouco malcheirosos.

@Jack-Barry isso funciona:

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

No entanto, ainda não é ideal, pois os construtores são públicos, portanto, é possível apenas fazer new Automobile({ make: "Audi" }) .

@elderapo Acho que isso me dá o que preciso - o exemplo é obviamente inventado, mas vejo onde minha falta de compreensão de _Generics_ me mordeu um pouco. Obrigado por esclarecer!

Espero que isso ajude os outros, deixe-me saber se há espaço para melhorias. Não é perfeito, pois as funções de leitura podem ficar fora de sincronia com as digitações, mas é o mais próximo que pude chegar do padrão que precisávamos.

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

Você pode facilmente estender esse padrão para as ações CRUD restantes.

No exemplo de @Jack-Barry, tento fazer com que a função read retorne uma instância da classe. Eu esperava que o seguinte funcionasse:

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

mas em vez disso, obtenha o erro Property 'create' does not exist on type 'ThisClass<A, T>'. Alguém teria uma solução alternativa sobre como resolver isso?

@everhardt Como o método create é static , ele precisaria ser chamado no class de this , não na instância. Dito isso, eu realmente não tenho uma boa solução em cima da minha cabeça para acessar a função class dinamicamente que ainda fornece a verificação de tipo desejada.

O mais próximo que posso chegar não é particularmente elegante e não é capaz de usar o type existente de BaseClass.create , então eles podem facilmente ficar fora de sincronia:

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

Eu _ não _ testei isso.

@Jack-Barry então você ganha this.constructor.create is not a function .

Isso faz sentido: Typescript está interpretando this como a instância, mas para o eventual analisador javascript, this é a classe, pois a função é estática.

Talvez não seja a extensão mais inspirada do exemplo de Jack-Barry, mas neste Link do Playground você pode ver como resolvi.

O ponto crucial:

  • o tipo ThisClass deve descrever todas as propriedades e métodos estáticos que os métodos de BaseClass (tanto estáticos quanto na instância) desejam usar com um 'this' polimórfico.
  • ao usar uma propriedade ou método estático em um método de instância, use (this.constructor as ThisClass<A, this>)

É um trabalho duplo, pois você precisa definir os tipos de métodos e propriedades estáticos tanto na classe quanto no tipo ThisClass , mas para mim funciona.

edit: corrigiu o link do playground

O PHP corrigiu esse problema há muito tempo. auto. estático. isto.

Olá a todos, é 2020, tendo _este_ problema. 😛

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

Eu esperaria que bugs fosse uma instância de Bunny .

Qual é a solução alternativa atual? Eu tentei de tudo neste problema, nada parece corrigir.

Atualização : Isso funcionou, faltou a definição do argumento 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

O problema com isso é que getters e setters não podem ter parâmetros de tipo.

Também é muito verboso.

Estou lidando com um problema semelhante. Para implementar uma "classe interna polimórfica", o melhor que encontrei é o seguinte:

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

Parece funcionar bem, mas a digitação de createInnerClass() é muito detalhada na minha opinião, e terei muitos deles na minha base de código. Alguém tem alguma ideia de como simplificar esse código?

O problema com essa abordagem é que ela não funciona com getters e setters estáticos.

Que tal implementar self. para propriedades estáticas? Eu tive esse problema há 4 anos e estou voltando ao mesmo tópico com o mesmo problema.

@sudomaxime Isso está fora do escopo deste problema e é contrário aos objetivos de design do TypeScript , desde que o ECMAScript não suporte nativamente self. como um alias para this.constructor. nas classes, o que é improvável acontece dado que self é um nome de variável comum.

A solução de @abdullah-kasim parece funcionar conforme descrito, mas não consigo fazê-la funcionar com classes genéricas:

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 =  )
   |   `  |
  /|       |\
  \| |   | |/
 (_|_|___|_|_)
   '"'   '"'

Eu apreciaria qualquer opinião sobre se isso é viável, pois eu mexi um pouco com isso sem sorte.

@stevehanson Isso funcionaria para você?

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

Testei isso no playground datilografado.

Inspirado neste link:
https://selleo.com/til/posts/gll9bsvjcj-generic-with-class-as-argument-and-returning-instance

E não tenho certeza de como isso conseguiu implicar que T é a própria classe . EDIT: Ah, lembrei, conseguiu implicar T porque o parâmetro silencioso this sempre passará a própria classe.

@abdullah-kasim isso funcionou! Uau, obrigado! Para o meu caso específico, meu construtor recebe um argumento, então foi assim que pareceu para mim, caso ajude mais alguém:

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

Algumas das soluções aqui não funcionam para getters estáticos porque não consigo passar nenhum argumento:

Dado o exemplo abaixo, como posso mover o getter estático default para uma classe pai?

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

const x = Letters.default.alpha;

Isso pode ser algo que vale a pena considerar se considerarmos melhorias de sintaxe.

Possivelmente relacionado: estou tendo dificuldade em adicionar outra camada de abstração ao exemplo de @abdullah-kasim. Essencialmente, eu gostaria de poder transformar Animal em uma interface e permitir que Bunny defina seu próprio create() estático.

Aqui está um exemplo prático (perdoe-me por abandonar a analogia do coelho!). Eu quero ser capaz de definir alguns tipos de documentos diferentes, e cada um deve ser capaz de definir um método de fábrica estático que transforma uma string em uma instância de documento. Eu quero impor que um tipo de documento deve definir apenas um analisador para si mesmo, e não para outros tipos de documento.

(os tipos no exemplo abaixo não estão certos - espero que esteja claro o suficiente o que estou tentando realizar)

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

(Eu acho) Eu gostaria de poder escrever algo assim:

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

Alguma idéia se uma solução alternativa é possível aqui também?

EDIT: Possível solução alternativa

Outro caso de uso no StackOverflow
Para projetar uma classe de tabela de pesquisa genérica.

// 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 comentou em 29 de maio de 2016
😰 Não acredito que isso ainda não foi corrigido/adicionado.....

ha ha
2020 chegou!

Isso é ridículo e insano. É 2020. 4 anos depois, por que isso não foi corrigido?

Por que não implementar algo como

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
Esta página foi útil?
0 / 5 - 0 avaliações