Typescript: Polimorfik "ini" untuk anggota statis

Dibuat pada 1 Des 2015  ·  150Komentar  ·  Sumber: microsoft/TypeScript

Saat mencoba menerapkan sistem model gaya rekaman aktif yang cukup mendasar, tetapi polimorfik, kami mengalami masalah dengan sistem tipe yang tidak mematuhi this saat digunakan bersama dengan konstruktor atau templat/generik.

Saya telah memposting sebelumnya tentang ini di sini, #5493, dan #5492 tampaknya menyebutkan perilaku ini juga.

Dan inilah postingan SO yang saya buat ini:
http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved

Saya telah mendaur ulang contoh saya dari #5493 ke dalam tiket ini untuk diskusi lebih lanjut. Saya ingin tiket terbuka yang mewakili keinginan untuk hal seperti itu dan untuk diskusi tetapi dua lainnya ditutup.

Berikut adalah contoh yang menguraikan model Factory yang menghasilkan model. Jika Anda ingin menyesuaikan BaseModel yang kembali dari Factory Anda harus dapat menimpanya. Namun ini gagal karena this tidak dapat digunakan di anggota statis.

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

Mungkin masalah ini konyol dan ada solusi yang mudah. Saya menyambut baik komentar mengenai bagaimana pola seperti itu dapat dicapai.

In Discussion Suggestion

Komentar yang paling membantu

Apakah ada alasan mengapa masalah ini ditutup?

Fakta bahwa polimorfik ini tidak berfungsi pada statika pada dasarnya membuat fitur ini DOA, menurut saya. Saya sampai saat ini tidak pernah benar-benar membutuhkan polimorfik ini pada anggota instan, namun saya membutuhkan setiap beberapa minggu pada statika, karena sistem penanganan statika diselesaikan jauh di masa-masa awal. Saya sangat senang ketika fitur ini diumumkan, kemudian kecewa ketika menyadari itu hanya berfungsi pada anggota instans.

Kasus penggunaan sangat mendasar dan sangat umum. Pertimbangkan metode pabrik sederhana:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

Pada titik ini saya telah menggunakan metode casting jelek atau membuang sampah sembarangan static create() di setiap pewaris. Tidak memiliki fitur ini sepertinya lubang kelengkapan yang cukup besar dalam bahasa.

Semua 150 komentar

Ukuran dan bentuk perahu saya sangat mirip! Ah!

Metode pabrik di superclass mengembalikan instance baru dari subclass-nya. Fungsionalitas kode saya berfungsi tetapi mengharuskan saya untuk memberikan tipe pengembalian:

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.

Saya mengalami masalah ini hari ini juga!

Solusi perbaikan adalah dengan meneruskan tipe anak sebagai generik yang memperluas kelas dasar, yang merupakan solusi yang saya terapkan untuk saat ini:

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

Apakah ada alasan mengapa masalah ini ditutup?

Fakta bahwa polimorfik ini tidak berfungsi pada statika pada dasarnya membuat fitur ini DOA, menurut saya. Saya sampai saat ini tidak pernah benar-benar membutuhkan polimorfik ini pada anggota instan, namun saya membutuhkan setiap beberapa minggu pada statika, karena sistem penanganan statika diselesaikan jauh di masa-masa awal. Saya sangat senang ketika fitur ini diumumkan, kemudian kecewa ketika menyadari itu hanya berfungsi pada anggota instans.

Kasus penggunaan sangat mendasar dan sangat umum. Pertimbangkan metode pabrik sederhana:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

Pada titik ini saya telah menggunakan metode casting jelek atau membuang sampah sembarangan static create() di setiap pewaris. Tidak memiliki fitur ini sepertinya lubang kelengkapan yang cukup besar dalam bahasa.

@paul-go masalahnya belum selesai... ?

@ paul-go Saya juga frustrasi dengan masalah ini, tetapi di bawah ini adalah solusi paling andal yang saya temukan. Setiap subkelas Hewan perlu memanggil super.create() dan hanya memasukkan hasilnya ke tipenya. Bukan masalah besar dan itu adalah satu liner yang dapat dengan mudah dihapus dengan ini ditambahkan.

Kompiler, intellisense, dan yang paling penting kelinci semuanya senang.

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 Ups ... untuk beberapa alasan saya bingung ini dengan #5862 ... maaf atas agresi kapak perang :-)

@Think7 Ya ... karenanya "menggunakan metode casting yang jelek atau membuang static create() di setiap pewaris". Ini cukup sulit meskipun ketika Anda seorang pengembang perpustakaan dan Anda tidak dapat benar-benar memaksa pengguna akhir untuk menerapkan banyak metode statis yang diketik di kelas yang mereka warisi dari Anda.

hukum Benar-benar melewatkan semua yang ada di bawah kode Anda: D

Meh tidak sia-sia, Harus menggambar kelinci.

:+1: kelinci

:kelinci: :hati:

+1, pasti ingin melihat ini

Apakah ada pembaruan diskusi tentang topik ini?

Itu tetap pada backlog saran kami yang sangat besar.

Javascript sudah bertindak dengan benar dalam pola seperti itu. Jika TS bisa mengikuti juga itu akan menyelamatkan kita dari banyak boilerplate/kode tambahan. "Pola model" cukup standar, saya berharap TS berfungsi seperti yang dilakukan JS dalam hal ini.

Saya juga sangat menyukai fitur ini untuk alasan "Model CRUD" yang sama seperti orang lain. Saya membutuhkannya pada metode statis lebih dari metode instan.

Ini akan memberikan solusi yang rapi untuk masalah yang dijelaskan di #8164.

Ada baiknya bahwa ada "solusi" dengan penggantian dan generik, tetapi mereka tidak benar-benar menyelesaikan apa pun di sini – seluruh tujuan memiliki fitur ini adalah untuk menghindari penggantian / casting seperti itu dan menciptakan konsistensi dengan bagaimana this kembali jenis ditangani dalam metode contoh.

Saya sedang mengerjakan pengetikan untuk Sequelize 4.0 dan itu menggunakan pendekatan di mana Anda mensubklasifikasikan kelas Model . Kelas Model itu memiliki metode statis yang tak terhitung jumlahnya seperti findById() dll. yang tentu saja tidak mengembalikan Model tetapi subkelas Anda, alias this dalam konteks statis:

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

Ini tidak mungkin untuk mengetik saat ini. Sequelize adalah _the_ ORM untuk Node dan sayangnya tidak dapat diketik. Sangat membutuhkan fitur ini. Satu-satunya cara adalah mentransmisikan setiap kali Anda memanggil salah satu dari fungsi-fungsi ini atau menimpa setiap fungsi tersebut, mengadaptasi tipe pengembalian dan tidak melakukan apa pun selain memanggil super.method() .

Juga jenis terkait adalah bahwa anggota statis tidak dapat mereferensikan argumen tipe generik - beberapa metode mengambil objek literal atribut untuk model yang dapat diketik melalui argumen tipe, tetapi mereka hanya tersedia untuk anggota instan.

Tidak percaya ini masih belum diperbaiki / ditambahkan.....

Kita bisa memanfaatkan ini dengan baik:

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

declare class UIButton extends NSObject {
}

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

di sini adalah kasus penggunaan yang saya harap berhasil (dimigrasikan dari https://github.com/Microsoft/TypeScript/issues/9775 yang saya tutup untuk ini)

Saat ini parameter konstruktor tidak dapat menggunakan tipe this di dalamnya:

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

Diharapkan: ini tersedia untuk parameter konstruktor.

Masalah yang ingin dipecahkan:

  • dapat mendasarkan API lancar saya baik di sekitar API lancar lainnya menggunakan kembali kode yang sama:
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();

Solusi masa depan:

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

Dengan cara ini, ketika TypeScript mendukung this untuk anggota statis, kode pengguna baru akan menjadi Baz.create() alih-alih Baz.create<Baz>() sementara kode pengguna lama akan berfungsi dengan baik. :senyum:

Ini sangat dibutuhkan! terutama untuk DAO yang memiliki metode statis yang mengembalikan instance. Sebagian besar dari mereka didefinisikan pada basis DAO, seperti (simpan, dapatkan, perbarui, dll ...)

Saya dapat mengatakan bahwa itu mungkin menyebabkan kebingungan, memiliki this mengetik pada metode statis menyelesaikan ke kelas itu dan bukan jenis kelas (yaitu: typeof ).

Dalam JS nyata, memanggil metode statis pada suatu tipe akan menghasilkan this di dalam fungsi menjadi kelas dan bukan turunannya... jadi tidak konsisten.

Namun, dalam intuisi orang, saya pikir hal pertama yang muncul ketika melihat tipe pengembalian this pada metode statis adalah tipe instans...

@shlomiassaf Itu tidak akan tidak konsisten. Saat Anda menentukan kelas sebagai tipe pengembalian untuk fungsi seperti Pengguna, tipe pengembalian akan menjadi turunan dari pengguna. Persis sama, ketika Anda mendefinisikan tipe pengembalian this pada metode statis, tipe pengembalian akan menjadi turunan dari this (instance dari kelas). Sebuah metode yang mengembalikan kelas itu sendiri kemudian dapat dimodelkan dengan tipe ini.

@felixfbecker ini benar-benar sudut pandang, ini adalah bagaimana Anda memilih untuk melihatnya.

Mari kita periksa apa yang terjadi di JS, sehingga kita dapat menyimpulkan logika:

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

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

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

Sekarang, this dalam runtime nyata adalah konteks terbatas dari fungsi, inilah yang terjadi dalam antarmuka seperti api yang lancar dan polimorfik fitur ini membantu dengan mengembalikan tipe yang tepat, yang hanya menyelaraskan dengan cara kerja JS, Kelas dasar yang mengembalikan this mengembalikan instance yang dibuat oleh kelas turunan. Fitur ini sebenarnya adalah perbaikan.

Untuk menyimpulkan:
Metode yang mengembalikan this yang didefinisikan sebagai bagian dari prototipe (instance) diharapkan mengembalikan instance.

Melanjutkan logika itu, metode yang mengembalikan this yang didefinisikan sebagai bagian dari tipe kelas (prototipe) diharapkan mengembalikan konteks terikat, yang merupakan tipe kelas.

Sekali lagi, tidak ada bias, tidak ada opini, fakta sederhana.

Dari segi intuisi, saya akan merasa nyaman memiliki this yang dikembalikan dari fungsi statis mewakili instance karena itu didefinisikan dalam tipe, tapi itu saya. Orang lain mungkin berpikir secara berbeda dan kita tidak bisa mengatakan bahwa mereka salah.

Masalahnya adalah mungkin untuk mengetik metode statis yang mengembalikan instance kelas dan metode statis yang mengembalikan kelas itu sendiri (ketik ini). Logika Anda masuk akal dari perspektif JS, tetapi kita berbicara tentang return _types_ di sini, dan menggunakan kelas sebagai tipe pengembalian (ini ini) dalam TypeScript dan bahasa lain selalu berarti turunan dari kelas. Untuk mengembalikan kelas yang sebenarnya, kita memiliki operator typeof.

@felixfbecker Ini menimbulkan masalah lain!

Jika tipe this adalah tipe instans, itu berbeda dari apa yang dirujuk oleh kata kunci this di badan metode statis. return this menghasilkan tipe pengembalian fungsi typeof this , yang benar-benar aneh!

Tidak. Ketika Anda mendefinisikan metode seperti

getUser(): User {
  ...
}

Anda berharap mendapatkan _instance_ Pengguna kembali, bukan kelas Pengguna (untuk itulah typeof ). Begitulah cara kerjanya di setiap bahasa yang diketik. Jenis semantik hanya berbeda dari semantik runtime.

Mengapa tidak menggunakan kata kunci this atau static sebagai konstruktor dalam fungsi untuk memanipulasi dengan kelas anak?

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

Dan jika kita membandingkan ini dengan contoh di JS, kita akan melihat apa yang berfungsi dengan benar:

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

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

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

Anda tidak dapat menggunakan tipe this pada metode statis, saya pikir itulah akar masalahnya.

@felixfbecker

Pertimbangkan ini:

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

Anotasi tipe ini intuitif, tetapi salah jika tipe this dalam metode statis adalah instance kelas. Kata kunci this memiliki tipe typeof this dalam metode statis!

Saya merasa bahwa this _should_ merujuk ke tipe instance, bukan tipe kelas, karena kita bisa mendapatkan referensi ke tipe kelas dari tipe instance ( typeof X ) tetapi tidak sebaliknya ( instancetypeof X ?)

@xealot oke, mengapa tidak menggunakan kata kunci static sebagai gantinya this ? Tetapi this dalam konteks statis JS masih menunjuk ke konstruktor.

@izatop ya, javascript yang dihasilkan berfungsi (dengan benar atau tidak benar). Namun, ini bukan tentang Javascript. Keluhannya bukan karena bahasa Javascript tidak mengizinkan pola ini, tetapi sistem tipe TypeScript tidak.

Anda tidak dapat menjaga keamanan tipe dengan pola ini karena TypeScript tidak mengizinkan tipe polimorfik this pada anggota statis. Ini termasuk metode kelas yang didefinisikan dengan kata kunci static dan constructor .

Tautan Taman Bermain

@LPGhatguy tetapi Anda membaca komentar saya sebelumnya, kan?! Jika Anda memiliki metode dengan tipe pengembalian User , Anda mengharapkan return new User(); , bukan return User; . Dengan cara yang sama, nilai kembalian this harus berarti return new this(); dalam metode statis. Dalam metode non-statis ini berbeda, tetapi jelas karena this selalu merupakan objek, Anda tidak akan dapat membuat instance. Tetapi pada akhirnya, pada dasarnya kita semua setuju bahwa ini adalah sintaks terbaik karena typeof dan fitur ini sangat dibutuhkan di TS.

@xealot saya mengerti apa TypeScript tidak mengizinkan polimorfik this , namun saya akan bertanya mengapa tidak menambahkan fitur ini ke TS?

Saya tidak tahu apakah yang berikut ini akan berfungsi untuk semua kasus penggunaan masalah ini, tetapi saya telah menemukan bahwa menggunakan tipe this: dalam metode statis bersama dengan penggunaan cerdas dari sistem inferensi tipe memungkinkan untuk bersenang-senang mengetik. Ini mungkin tidak terlalu mudah dibaca, tetapi berhasil tanpa harus mendefinisikan ulang metode statis di kelas anak-anak.

Bekerja dengan [email protected]

Pertimbangkan hal berikut;

// 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 mengapa this diberikan sebagai parameter?

Ini karena https://github.com/Microsoft/TypeScript/issues/3694 yang dikirimkan dengan TypeScript 2.0.0 yang memungkinkan mendefinisikan parameter ini untuk fungsi (sehingga memungkinkan penggunaan this di tubuhnya tanpa masalah dan juga untuk mencegah penggunaan fungsi yang salah)

Ternyata kita dapat menggunakannya pada metode serta metode statis, dan itu memungkinkan untuk hal-hal yang sangat lucu, seperti menggunakannya untuk "membantu" penyimpul tipe dengan memaksanya untuk membandingkan this yang kami sediakan (IModel) dengan yang digunakan (Anjing, atau Kucing). Ia kemudian "menebak" jenis yang benar untuk T dan menggunakannya, dengan demikian mengetikkan fungsi kita dengan benar.

(Perhatikan bahwa this adalah parameter _special_ yang dipahami oleh kompiler TypeScript, ia tidak menambahkan satu lagi ke fungsi tersebut)

Saya pikir sintaksnya seperti <this extends Whatever> . Jadi ini masih akan berfungsi jika create() memiliki parameter lain?

Sangat

Saya juga ingin menunjukkan bahwa ini penting untuk tipe bawaan.
Ketika Anda mensubkelaskan Array misalnya, metode from() dari subkelas akan mengembalikan turunan dari subkelas, bukan 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

Setiap pembaruan tentang masalah ini? Fitur ini akan sangat meningkatkan pengalaman tim saya dengan TypeScript.

Harap dicatat bahwa ini agak kritis karena ini adalah pola yang umum digunakan dalam ORM dan banyak kode berperilaku sangat berbeda dari yang dipikirkan oleh kompiler dan sistem tipe.

Saya akan mengajukan beberapa argumen kontra terhadap argumen kontra.

Seperti yang ditunjukkan oleh @shlomiassaf , mengizinkan this dalam anggota statis akan menyebabkan kebingungan karena this berarti hal yang berbeda dalam metode statis dan metode instan. Selanjutnya, this dalam anotasi tipe dan this dalam badan metode memiliki semantik yang berbeda. Kami dapat menghindari kebingungan ini dengan memperkenalkan kata kunci baru, tetapi biaya kata kunci baru tinggi.

Dan ada solusi mudah di TypeScript saat ini. @ceymard sudah menunjukkan itu . Dan saya lebih suka pendekatannya karena itu menangkap semantik runtime dari metode statis. Pertimbangkan kode ini.

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

Jika polimorfik ini didukung, kompiler harus melaporkan kesalahan dalam class B extends A . Tapi ini adalah perubahan yang menghancurkan.

@HerringtonDarkholme Saya tidak melihat kebingungan, tipe pengembalian this persis seperti yang saya harapkan dari return new this() . Kemungkinan kita bisa menggunakan self untuk itu, tetapi memperkenalkan kata kunci lain (selain yang digunakan untuk menghasilkan output) tampaknya lebih membingungkan bagi saya. Kami sudah menggunakannya sebagai tipe pengembalian, satu-satunya masalah (tapi besar) adalah tidak didukung di anggota statis.

Kompiler seharusnya tidak mengharuskan pemrogram untuk melakukan solusi, TypeScript seharusnya menjadi "JavaScript yang disempurnakan" dan itu tidak benar dalam kasus ini, Anda harus melakukan solusi kompleks agar kode Anda tidak menghasilkan jutaan kesalahan. Sangat menyenangkan bahwa kami dapat mencapainya dalam versi saat ini, tetapi itu tidak berarti itu baik-baik saja dan tidak perlu diperbaiki.

Mengapa tipe pengembalian this berbeda dengan tipe this di badan metode?
Mengapa kode ini gagal? Bagaimana Anda bisa menjelaskannya kepada pendatang baru?

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

Mengapa superset JavaScript gagal menerima ini?

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

abstract class B extends A {}

Benar, kita mungkin perlu memperkenalkan kata kunci lain. Bisa jadi self , atau mungkin instance

@HeringtonDarkholme Inilah cara saya menjelaskannya kepada pendatang baru.
Saat kamu melakukan

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

tipe pengembalian B berarti Anda harus mengembalikan instance B . Jika Anda ingin mengembalikan kelas itu sendiri, Anda perlu menggunakan

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

Sekarang, seperti di JavaScript, this di anggota statis merujuk ke kelas. Jadi, jika Anda menggunakan tipe pengembalian this dalam metode statis, Anda harus mengembalikan instance kelas:

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

jika Anda ingin mengembalikan kelas itu sendiri, Anda perlu menggunakan typeof this :

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

begitulah cara kerja tipe di TS sejak saat itu. Jika Anda mengetik dengan kelas, TS mengharapkan sebuah instance. Jika Anda ingin mengetik untuk tipe kelas itu sendiri, Anda perlu menggunakan typeof .

Kemudian saya akan bertanya mengapa this tidak memiliki arti yang sama di badan metode. Jika this dalam anotasi tipe metode instance memiliki arti yang sama dengan this di badan metode yang sesuai, mengapa tidak sama untuk metode statis?

Kita dihadapkan pada dilema dan ada tiga pilihan:

  1. make this berarti hal yang berbeda dalam konteks yang berbeda (kebingungan)
  2. perkenalkan kata kunci baru seperti self atau static
  3. biarkan programmer menulis anotasi tipe yang lebih aneh (berdampak pada beberapa pengguna)

Saya lebih suka pendekatan ketiga karena mencerminkan semantik runtime definisi metode statis .
Itu juga menemukan kesalahan di situs penggunaan, daripada mendefinisikan situs, yaitu, seperti dalam komentar ini , kesalahan dilaporkan dalam B.create , bukan class B extends A . Kesalahan penggunaan situs lebih tepat dalam kasus ini. (pertimbangkan Anda mendeklarasikan metode statis yang merujuk ke this dan kemudian mendeklarasikan subkelas abstrak).

Dan, yang paling penting, tidak memerlukan proposal bahasa untuk fitur baru.

Memang preferensi berbeda dari orang. Tapi setidaknya saya ingin melihat proposal yang lebih detail seperti ini . Ada banyak masalah yang harus diselesaikan seperti penetapan kelas abstrak, tanda tangan konstruktor subkelas yang tidak kompatibel, dan strategi laporan kesalahan.

@HerringtonDarkholme Karena dalam kasus metode instance, runtime this adalah instance objek dan bukan kelas, jadi tidak ada ruang untuk kebingungan. Jelas bahwa tipe this secara harfiah adalah runtime this , tidak ada pilihan lain. Sementara dalam kasus statis itu berperilaku seperti anotasi kelas lainnya dalam bahasa pemrograman berorientasi objek apa pun: beri anotasi dengan sebuah kelas, dan Anda mengharapkan sebuah instance kembali. Plus dalam kasus TS, karena dalam kelas JavaScript adalah objek juga, ketik dengan typeof sebuah kelas dan Anda mendapatkan kelas literalnya kembali.

Maksud saya mungkin akan lebih konsisten ketika saat itu ketika mereka menerapkan this tipe pengembalian untuk juga memikirkan kasus statis dan sebagai gantinya mengharuskan pengguna untuk selalu menulis typeof this sebagai contoh metode. Tapi keputusan itu dibuat saat itu jadi kita harus bekerja dengannya sekarang, dan seperti yang saya katakan itu tidak benar-benar meninggalkan ruang untuk kebingungan karena this dalam metode instan tidak menghasilkan tipe lain (tidak seperti kelas, yang memiliki tipe statis dan tipe instans), sehingga berbeda dalam kasus itu dan tidak akan membingungkan siapa pun.

Oke, anggap saja tidak ada yang akan bingung dengan this . Bagaimana dengan penugasan kelas abstrak?

Statisitas metode agak sewenang-wenang. Setiap fungsi dapat memiliki properti dan metode. Jika itu juga dapat dipanggil dengan new , kami memilih untuk memanggil properti dan metode ini static . Sekarang konvensi ini telah disemen dengan kuat ke dalam ECMAScript.

@HerringtonDarkholme Anda tidak diragukan lagi benar bahwa penggunaan this akan menyebabkan kebingungan. Namun, karena tidak ada yang salah tentang menggunakan this saya akan mengatakan itu baik-baik saja, terutama ketika mempertimbangkan analisis biaya manfaat yang cerdik dari berbagai alternatif. Saya pikir this adalah alternatif yang paling tidak buruk.

Saya baru saja mencoba mengikuti utas ini dari posting asli saya, saya merasa mungkin ini sedikit keluar jalur.

Untuk memperjelas, keinginannya adalah agar anotasi tipe this menjadi polimorfik saat digunakan dalam konstruktor, khususnya sebagai generik/templat. Sehubungan dengan @shlomiassaf dan @HerringtonDarkholme , saya yakin contoh dengan metode static dapat membingungkan dan bukan itu maksud dari masalah ini.

Meskipun tidak sering dianggap demikian, konstruktor kelas adalah statis. Contoh (yang akan saya posting ulang dengan lebih banyak komentar klarifikasi) tidak mendeklarasikan this pada metode statis, melainkan mendeklarasikan this untuk penggunaan di masa mendatang melalui generik dalam jenis anotasi metode statis .

Perbedaannya adalah saya tidak ingin this segera dihitung pada metode statis, tetapi di masa depan dalam metode dinamis.

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

Bagi siapa pun yang menganggap ini membingungkan, yah, ini tidak super lurus ke depan, saya setuju. Namun, penggunaan this dalam konstruktor BaseModel sebenarnya bukanlah penggunaan statis, melainkan penggunaan yang ditangguhkan untuk dihitung nanti. Namun, metode statis (termasuk konstruktor) tidak beroperasi seperti itu.

Saya pikir saat runtime ini semua berfungsi seperti yang diharapkan. Namun sistem tipe tidak mampu memodelkan pola khusus ini. Ketidakmampuan TypeScript untuk memodelkan pola javascript adalah dasar mengapa masalah ini terbuka.

Maaf jika ini adalah komentar yang membingungkan, ditulis dengan tergesa-gesa.

@xealot Saya mengerti maksud Anda, tetapi masalah lain yang secara khusus menyarankan polimorfik this untuk metode statis ditutup sebagai duplikat dari yang ini. Saya berasumsi perbaikan di TS akan mengaktifkan kedua kasus penggunaan.

Apa kasus penggunaan Anda yang sebenarnya? Mungkin solusi 'ini' sudah cukup.

Saya menggunakannya di perpustakaan ORM khusus dengan sukses
Le jeu. 20 Oktober 2016 19:15, Tom Marius [email protected] a
écrit:

Harap dicatat bahwa ini agak kritis karena ini umum digunakan
pola dalam ORM dan banyak kode berperilaku sangat berbeda dari
kompiler dan sistem tipe berpikir itu seharusnya.


Anda menerima ini karena Anda disebutkan.
Balas email ini secara langsung, lihat di GitHub
https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -255169194,
atau matikan utasnya
https://github.com/notifications/unsubscribe-auth/AAtAoRHc45B386xdNhuCLB8iW6i82e7Uks5q16GzgaJpZM4GsmOH
.

Terima kasih kepada @ceymard dan @HeringtonDarkholme.

static create<T extends A>(this: {new (): T}) { return new this(); } melakukan trik untuk saya 👍

Ini lebih tidak jelas daripada static create(): this { return new this(); } pada pandangan pertama tapi setidaknya, itu benar! Ini secara akurat menangkap jenis this di dalam metode statis.

Saya harap ini entah bagaimana dapat diprioritaskan, meskipun bukan yang paling banyak di-upvote atau dikomentari dll. Sangat menjengkelkan harus menggunakan argumen tipe untuk mendapatkan tipe yang tepat, misalnya:

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

Ketika secara praktis mungkin untuk melakukan:

let u = User.create();

Dan tidak dapat dihindari bahwa static terkadang merujuk pada tipe instance.

Saya punya ide lain mengenai diskusi tentang apakah tipe this dalam anggota statis harus menjadi tipe sisi statis kelas atau sisi instance. this bisa menjadi sisi statis, itu akan cocok dengan apa yang dilakukan anggota instance dan apa perilaku runtime, dan kemudian Anda bisa mendapatkan jenis instance melalui this.prototype :

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

class User extends Model {}

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

ini akan sejalan dengan cara kerjanya di JavaScript. this adalah objek kelas itu sendiri, dan instance ada di this.prototype . Ini pada dasarnya adalah mekanik yang sama dengan tipe yang dipetakan, setara dengan this['prototype'] .

Dengan cara yang sama Anda dapat membayangkan sisi statis tersedia di anggota instan melalui this.constructor (Saya tidak dapat memikirkan kasus penggunaan untuk atm ini).

Saya juga ingin menyebutkan bahwa tidak ada solusi/peretasan yang disebutkan yang berfungsi untuk mengetik Model ORM di Sequelize.

  • Anotasi tipe this tidak dapat digunakan dalam konstruktor, sehingga tidak berfungsi untuk tanda tangan ini:
    ts abstract class Model { constructor(values: Partial<this.prototype>) }
  • Melewati tipe sebagai argumen kelas generik tidak berfungsi untuk metode statis, karena anggota statis tidak dapat mereferensikan parameter generik
  • Melewati tipe sebagai argumen metode generik berfungsi untuk metode, tetapi tidak berfungsi untuk properti statis, seperti:
    ts abstract class Model { static attributes: { [K in keyof this.prototype]: { type: DataType, field: string, unique: boolean, primaryKey: boolean, autoIncrement: boolean } }; }

+1 untuk permintaan ini. ORM saya akan jauh lebih bersih.

Semoga kita akan segera melihat solusi bersihnya

Sementara itu, terima kasih kepada semua yang menawarkan solusi!

Bisakah kita melangkah lebih jauh tentang ini karena tidak ada diskusi lagi?

Mau dibawa kemana? Ini masih merupakan cara di mana TypeScript tidak dapat dengan bersih memodelkan Javascript sejauh yang saya tahu dan belum melihat solusi selain tidak melakukannya.

Ini adalah fitur yang harus dimiliki oleh pengembang ORM, seperti yang kita lihat sudah ada tiga pemilik ORM populer yang meminta fitur ini.

@pleerock @xealot Solusi telah diusulkan di atas:

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

Saya menggunakan ini secara ekstensif. Deklarasi metode statis di kelas dasar agak berat tapi itulah harga yang harus dibayar untuk menghindari distorsi pada jenis this di dalam metode statis.

@pleerock

Saya memiliki ORM internal yang menggunakan pola this: secara ekstensif tanpa masalah.

Saya rasa tidak perlu melebih-lebihkan bahasanya ketika fitur tersebut sebenarnya sudah ada di sini meski harus diakui sedikit berbelit-belit. Kasus penggunaan untuk sintaks yang lebih jelas sangat terbatas dan dapat menimbulkan inkonsistensi.

Mungkin ada utas Stackoverflow dengan pertanyaan dan solusi ini untuk referensi?

@bjouhier @ceymard Saya menjelaskan mengapa semua solusi di utas ini tidak berfungsi untuk Sequelize: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -269463313

Dan ini bukan hanya tentang ORM, tetapi juga perpustakaan standar: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -244550725

@felixfbecker Baru saja memposting sesuatu tentang kasus penggunaan konstruktor dan menghapusnya (saya menyadari bahwa TS tidak mengamanatkan konstruktor di setiap subkelas setelah memposting).

@felixbecker Mengenai kasus konstruktor, saya menyelesaikannya dengan tetap menggunakan konstruktor tanpa parameter dan sebagai gantinya menyediakan metode statis khusus (buat, klon, ...). Lebih eksplisit dan lebih mudah diketik.

@bjouhier Itu berarti Anda harus mendeklarasikan ulang pada dasarnya setiap metode di setiap subkelas model. Lihat berapa banyak yang ada di Sequelize: http://docs.sequelizejs.com/class/lib/model.js~Model.html

Argumen saya adalah dari perspektif yang sama sekali berbeda. Meskipun ada solusi parsial dan agak tidak intuitif dan kami dapat berdebat dan tidak setuju apakah itu cukup atau tidak, apa yang dapat kami setujui bahwa ini adalah area di mana TypeScript tidak dapat dengan mudah memodelkan Javascript.

Dan pemahaman saya adalah bahwa TypeScript harus menjadi perpanjangan dari Javascript.

@felixfbecker

Itu berarti Anda harus mendeklarasikan ulang pada dasarnya setiap metode di setiap subkelas model.

Mengapa? Itu sama sekali bukan pengalaman saya. Bisakah Anda menggambarkan masalah dengan contoh kode?

@bjouhier Saya menggambarkan masalah secara mendalam dengan contoh kode dalam komentar saya di utas ini, sebagai permulaan https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -222348054

Tapi lihat contoh create saya di atas . Metode create adalah metode statis. Itu tidak dideklarasikan ulang namun diketik dengan benar di subclass. Mengapa metode Model perlu didefinisikan ulang?

@felixfbecker Anda _untuk contoh awal_:

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 Oke, jadi sepertinya anotasi this: { new (): T } benar-benar membuat inferensi tipe berfungsi. Yang membuat saya bertanya-tanya mengapa ini bukan tipe default yang digunakan kompiler.
Solusinya tentu saja tidak bekerja untuk konstruktor, karena mereka tidak dapat memiliki parameter this .

Ya, ini tidak berfungsi untuk konstruktor, tetapi Anda dapat mengatasi masalah dengan sedikit perubahan API, dengan menambahkan metode create statis.

Saya mengerti itu, tetapi ini adalah pustaka JavaScript, jadi kita berbicara tentang mengetikkan API yang ada dalam sebuah deklarasi.

Jika this berarti { new (): T } di dalam metode statis, maka this tidak akan menjadi tipe yang tepat untuk this di dalam metode statis. Misalnya new this() tidak akan diizinkan. Untuk konsistensi, this harus tipe fungsi konstruktor, bukan tipe instance kelas.

Saya mendapatkan masalah dengan mengetik perpustakaan yang ada. Jika Anda tidak memiliki kendali atas perpustakaan itu, Anda masih dapat membuat kelas dasar perantara ( BaseModel extends Model ) dengan fungsi create yang diketik dengan benar dan mendapatkan semua model Anda dari BaseModel .

Jika Anda ingin mengakses properti statis dari kelas dasar, Anda dapat menggunakan

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

Tetapi Anda mungkin memiliki poin yang valid tentang konstruktor. Saya tidak melihat alasan kuat mengapa kompiler harus menolak yang berikut:

constructor(values: Partial<this>) {}

Saya setuju dengan @xealot bahwa ada rasa sakit di sini. Akan sangat keren jika kita bisa menulis

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

dari pada

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

Tetapi kita membutuhkan operator baru dalam sistem pengetikan ( instanceof ?). Berikut ini tidak valid:

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

TS 2.4 memecahkan solusi ini untuk saya:
https://travis-ci.org/types/sequelize/builds/247636686

@sandersn @felixfbecker Saya pikir ini adalah bug yang valid. Namun, saya tidak dapat mereproduksinya dengan cara yang minimal. Parameter panggilan balik bertentangan.

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

Adakah kemungkinan ini akan diperbaiki di beberapa titik?

Saya mengalami ini hari ini juga. Pada dasarnya saya ingin membangun kelas dasar tunggal, seperti ini:

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

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

Penggunaan akan menjadi seperti:

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

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

Saya mencoba sekitar 10 variasi ini, dengan varian StaticThis yang disebutkan di atas, dan banyak lainnya. Pada akhirnya hanya ada satu versi yang akan dikompilasi, tetapi kemudian hasil dari getInstance() diturunkan hanya sebagai Object .

Saya merasa seperti ini jauh lebih sulit daripada yang dibutuhkan untuk bekerja dengan konstruksi seperti ini.

Karya-karya berikut:

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

Bagian this as any as typeof Singleton jelek tetapi ini menunjukkan bahwa kita membodohi sistem tipe karena _instance harus disimpan pada konstruktor dari kelas turunan.

Biasanya, kelas dasar akan dikubur dalam kerangka kerja Anda sehingga tidak banyak merugikan jika implementasinya agak jelek. Yang penting adalah kode bersih di kelas turunan.

Saya berharap dalam 2,8 nightlies untuk dapat melakukan sesuatu seperti ini:

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

Sayangnya tidak ada keberuntungan seperti itu :(

@bjouhier contoh kode bekerja seperti pesona. Tetapi pecahkan pelengkapan otomatis IntelliSense.

React 16.3.0 dirilis dan tampaknya mustahil untuk mengetikkan metode baru getDerivedStateFromProps dengan benar, mengingat metode statis tidak dapat mereferensikan parameter tipe kelas:

(contoh sederhana)

class Component<P, S> {

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

    props: P
    state: S
}

Apakah ada solusi? (Saya tidak menghitung "mengetik P dan S sebagai PP dan SS dan berharap pengembang akan membuat kedua keluarga tipe ini benar-benar cocok" sebagai satu: p)

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

@cncolder Saya telah menyaksikan pemecahan intellisense juga. Saya percaya ini dimulai dengan TypeScript 2.7.

Satu hal yang tidak saya lihat di sini, memperluas contoh Singleton - itu akan menjadi pola umum untuk membuat kelas Singleton/Multiton memiliki konstruktor yang dilindungi - dan ini tidak dapat diperpanjang jika tipenya adalah { new(): T } seperti ini memberlakukan konstruktor publik - istilah this atau solusi lain yang disediakan di sini ( instanceof this , typeof this dll) harus menyelesaikan masalah itu. Ini pasti mungkin dengan melemparkan kesalahan pada konstruktor jika lajang tidak meminta pembuatannya, dll. tetapi itu mengalahkan tujuan kesalahan pengetikan - untuk memiliki solusi runtime...

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

Saya tidak tahu mengapa Anda membutuhkan singleton di TS dengan getInstance() , Anda bisa mendefinisikan objek literal. Atau jika Anda benar-benar ingin menggunakan kelas (untuk anggota pribadi), ekspor hanya instance kelas dan bukan kelas, atau gunakan ekspresi kelas ( export new class ... ).

Ini lebih berguna untuk Multiton - dan saya yakin saya telah menemukan pola yang berfungsi,... meskipun masih terasa sangat kacau tanpa tipe ini.. alih-alih ini perlu

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

Saya juga memiliki beberapa kode untuk menyimpan kunci sehingga dapat diakses di kelas anak saat pembuatan, tetapi saya telah menghapusnya untuk kesederhanaan...

Hai. Saya mengalami masalah yang membutuhkan "ini" polimorfik untuk konstruktor. Mirip dengan komentar ini.

Saya ingin membuat kelas. Kelas itu sendiri memiliki cukup banyak properti sehingga konstruktor tidak akan terlihat bagus jika semua properti kelas itu diinisialisasi melalui parameter konstruktor.

Saya lebih suka konstruktor yang menerima objek untuk menginisialisasi properti. Sebagai contoh:

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

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

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

Kemudian, saya ingin memperluas kelas. Sebagai contoh:

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

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

Akan sangat menyenangkan jika tipe this dapat digunakan pada konstruktor juga.

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

Jadi, IntelliSense dapat menyimpulkan properti dari kelas anak tanpa boilerplate tambahan: Misalnya, mendefinisikan ulang konstruktor. Ini akan terlihat lebih alami daripada menggunakan metode statis untuk menginisialisasi kelas.

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

Terima kasih banyak untuk pertimbangan Anda.

Saya senang saya menemukan utas ini. Sejujurnya saya pikir ini adalah masalah yang sangat serius dan sangat tidak disarankan untuk melihatnya terbawa hingga 3.0.

Melihat semua solusi dan mempertimbangkan berbagai solusi yang saya mainkan selama beberapa tahun terakhir, saya sampai pada kesimpulan bahwa perbaikan cepat apa pun yang kami coba akan gagal jika dan ketika ada solusi. Dan sampai saat itu, setiap paksaan yang kami buat hanya membutuhkan terlalu banyak pekerjaan manual untuk mempertahankannya di seluruh proyek. Dan setelah diterbitkan, Anda tidak dapat meminta konsumen untuk memahami pola pikir Anda sendiri dan mengapa Anda memilih untuk memperbaiki sesuatu yang TypeScript jika mereka hanya menggunakan paket Anda dalam vanilla tetapi seperti kebanyakan dari kita, bekerja dengan VSCode.

Pada saat yang sama, menulis kode untuk membuat Sistem Tipe senang ketika bahasa sebenarnya tidak menjadi masalah bertentangan dengan alasan yang sama untuk menggunakan fitur Ketik untuk memulai.

Rekomendasi saya kepada semua orang adalah menerima bahwa squigglies salah dan menggunakan //@ts-ignore because my code works as expected bila memungkinkan.

Jika linter Anda kebal terhadap kecerdasan manusia yang masuk akal, inilah saatnya untuk menemukan yang tahu tempatnya.

PEMBARUAN :
Saya ingin menambahkan upaya saya sendiri ke inferensi statis yang mungkin-aman-menambah . Pendekatan ini sangat ambien karena dimaksudkan untuk keluar-dari-jalan. Ini sangat eksplisit, memaksa Anda untuk memeriksa augmentasi Anda dan tidak hanya berasumsi bahwa warisan akan bekerja untuk Anda.

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

Jelas saya tidak dapat menjamin pola ini berlaku, juga tidak akan memenuhi setiap skenario di utas ini, tetapi untuk saat ini, itu berfungsi seperti yang diharapkan.

Pilihan ini berasal dari kesadaran bahwa sampai saat ini file lib TypeScript sendiri hanya menyertakan dua deklarasi kelas!

SafeArray

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

VarDate

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

Dibahas sebentar hari ini. Poin kunci:

  • Apakah this merujuk ke sisi instance atau sisi statis?

    • Pasti sisi statis. Sisi instance dapat dirujuk melalui InstanceTypeOf tetapi kebalikannya tidak benar. Ini juga mempertahankan simetri bahwa this di signature memiliki tipe yang sama dengan this di body.

  • Masalah kesehatan

    • Tidak ada jaminan bahwa daftar parameter konstruktor kelas turunan memiliki hubungan apa pun dengan daftar parameter konstruktor kelas dasarnya

    • Kelas sangat sering merusak substitusi sisi statis dalam hal ini

    • Penggantian sisi non- konstruksi diberlakukan dan ini biasanya yang diminati orang

    • Kami telah mengizinkan pemanggilan new this() yang tidak sehat dalam metode static

    • Mungkin tidak ada yang peduli dengan sisi konstruksi tanda tangan dari static this dari luar?

Solusi yang ada:

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

Khususnya, ini hanya berfungsi jika tanda tangan konstruk Bar dapat diganti dengan benar untuk Foo

@RyanCavanaugh Saya telah mencoba memberikan penjelasan dari mana nilai untuk this berasal dari contoh Anda static boo<T extends typeof Foo>(this: T): InstanceType<T> , tetapi saya tidak mengerti. Yang pasti saya membaca ulang semua yang ada di obat generik, tapi tetap saja - tidak. Jika saya mengganti this dengan clazz misalnya, itu tidak akan berfungsi lagi dan kompiler mengeluh dengan "Argumen yang diharapkan 1, tetapi mendapat 0." Jadi ada keajaiban yang terjadi di sana. Bisakah Anda menjelaskan? Atau arahkan saya ke arah yang benar dalam dokumentasi?

Sunting:
Juga, mengapa extends typeof Foo dan bukan hanya extends Foo ?

@creynders this adalah parameter palsu khusus yang dapat diketik untuk konteks fungsi. dokumen .

Menggunakan InstanceType<T> tidak berfungsi bila ada obat generik yang terlibat.

Kasus penggunaan saya terlihat seperti ini:

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

Tautan taman bermain TypeScript

@mastermatt oh wow, benar-benar TIDAK mengambilnya saat membaca dokumen... Terima kasih

Melihat contoh @RyanCavanaugh , saya bisa membuat metode statis kami berfungsi, tetapi saya juga memiliki metode instan yang merujuk ke metode statis, tetapi saya tidak tahu cara mengetik yang benar untuk itu.

Contoh:

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

Jika saya dapat menemukan solusi untuk masalah terakhir ini, saya akan siap sampai sesuatu yang resmi datang.

Perhatikan juga dengan contoh itu, jika subkelas mencoba mengganti metode statis apa pun dan kemudian memanggil super , maka semuanya juga akan kesal... Selain dua masalah ini, solusinya tampaknya berfungsi dengan baik. Meskipun, kedua masalah itu agak menghalangi kode kami.

IIRC ini karena ini tidak dapat diekspresikan dalam TypeScript tanpa sedikit membengkokkan sistem tipe. Seperti dalam ; this.constructor sama sekali tidak dijamin seperti yang Anda pikirkan atau sesuatu seperti itu.

Dalam kasus yang tepat, saya akan memiliki metode instance boo() mengembalikan this dan menipu sedikit di dalam, memaksa kompiler untuk menerima bahwa saya tahu apa yang saya lakukan.

Alasan umum saya adalah saya ingin API sesederhana mungkin untuk sisa kode, meskipun terkadang itu berarti sedikit curang.

Jika seseorang memiliki sesuatu yang lebih kuat, saya akan menyukainya

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

Anda juga dapat pergi ke rute antarmuka dan melakukan sesuatu seperti itu;

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

Ini curang juga, tapi mungkin sedikit lebih terkontrol? Saya tidak bisa mengatakannya.

Jadi argumen pertama, this dalam metode "boo" statis diperlakukan secara khusus, bukan sebagai argumen biasa? Adakah tautan ke dokumen, yang menjelaskan ini?

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

Saya mengalami masalah yang sama (setidaknya serupa).
Saya menggunakan Javascript Vanilla dengan JSDoc (bukan TypeScript), jadi saya tidak dapat menggunakan/menerapkan solusi seputar obat generik yang diusulkan di utas ini, tetapi root tampaknya sama.
Saya telah membuka edisi ini: #28880

Masalah ini benar-benar sudah 3 tahun.
Adakah yang menemukan solusi yang cocok yang bisa bekerja untuk saya?

Tiga tahun

Pekerjaan @RyanCavanaugh sangat bagus untuk metode statis, tetapi bagaimana dengan pengakses statis?

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

Tiga tahun

Oke, Anda setengah layak dalam mengurangi tanggal. Tapi bisakah Anda menawarkan solusi? ;)

@RyanCavanaugh Ini adalah masalah ketika kita menggunakan nilai this di generator sebagai berikut:

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

Saat kita membuat generator, kita tidak bisa menggunakan fungsi panah untuk menggunakan nilai this . Jadi sekarang kita harus mendeklarasikan tipe this secara eksplisit di subclass. Kasus sebenarnya adalah sebagai berikut:

      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

Kita tidak perlu mendeklarasikan tipe this di subclass jika kita bisa melakukannya di kelas dasar. Namun, nilai this tidak diinisialisasi ketika generator dipanggil secara sinkron di kelas super. Saya menghindari masalah ini dalam kode tetapi ini adalah peretasan yang kotor. Jadi pola ini awalnya mungkin tidak cocok dengan bahasa pemrograman berbasis kelas.

Bukan yang terbersih, tetapi mungkin berguna untuk mengakses kelas anak dari metode statis.

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

class C extends Base {
}

C.foo();

Meskipun saya tidak yakin apakah masalah yang saya hadapi saat ini adalah karena ini, tetapi setelah melihat sekilas masalah ini, sepertinya memang demikian. Mohon koreksinya jika tidak demikian.

Jadi saya memiliki 2 kelas berikut yang terkait melalui pewarisan.

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

Selanjutnya, saya menambahkan metode mapObject di kelas SubTarget sebagai berikut.

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

Meskipun saya berharap ini berfungsi, saya mendapatkan kesalahan berikut.

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

Apakah kesalahan ini dihasilkan karena masalah ini? Jika tidak, penjelasan akan sangat bagus.

Tautan ke taman bermain

Datang ke sini sambil mencari solusi untuk membubuhi keterangan metode statis yang mengembalikan instance baru dari kelas aktual yang dipanggil.

Saya pikir solusi yang disarankan oleh @SMotaal (dalam kombinasi dengan new this() ) adalah yang paling bersih dan paling masuk akal bagi saya. Ini memungkinkan untuk mengekspresikan maksud yang diinginkan, tidak memaksa saya untuk menentukan jenis pada setiap panggilan ke metode generik dan tidak menambahkan overhead apa pun dalam kode akhir.

Tapi serius, bagaimana ini belum menjadi bagian dari TypeScript inti? Ini skenario yang cukup umum.

@danielvy — Saya pikir pemutusan antara OOP dan prototipe adalah kesenjangan mental yang lebih besar yang gagal membuat semua orang bahagia. Saya pikir asumsi inti tentang kelas yang dibuat jauh sebelum fitur kelas berevolusi dalam spesifikasi tidak sejalan, dan melanggar dari itu adalah perangkap bagi banyak fitur TypeScript yang berfungsi, jadi semua orang "memprioritaskan" dan tidak apa-apa tetapi tidak untuk " jenis" dalam pandangan saya. Tidak memiliki solusi yang lebih baik dari kompetisi hanya masalah jika ada kompetisi, itu murni mengapa semua telur dalam satu keranjang selalu buruk bagi semua orang — saya bersikap pragmatis dan tulus di sini.

Ini tidak berfungsi:

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.

Tetapi bukankah ini seharusnya diizinkan karena Ekstensi memperluas Basis?

Perhatikan ini berfungsi:

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

tetapi kemudian Anda tidak dapat menggunakan this.member di dalamnya.

Jadi saya menduga dari @ntucker posting bahwa solusinya hanya bekerja satu level dalam kecuali kelas yang diperluas cocok dengan kelas dasar persis?

Halo! Bagaimana dengan konstruktor yang dilindungi ?

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

  protected constructor() {}
}

class B extends A {} 

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

Jika konstruktor bersifat publik - tidak apa-apa, tetapi jika tidak, itu menyebabkan kesalahan:

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.

Taman bermain - http://tiny.cc/r74c9y

Apakah ada harapan untuk ini? Baru saja menemukan kebutuhan ini sekali lagi :F

@ntucker Duuuuudee, kamu berhasil!!! Realisasi utama bagi saya adalah membuat metode pembuatan statis menjadi generik, mendeklarasikan this param, dan kemudian melakukan pemeran InstanceType<U> di akhir.

Tempat bermain

Dari atas ^

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 😄 

Ini sangat memenuhi kasus penggunaan saya, dan tampaknya pada TS 3.5.1 ini memenuhi sebagian besar ppl di utas ini juga (lihat taman bermain untuk eksplorasi prop statis theAnswer serta tingkat perpanjangan ke-3)

Pengambilan panas: Saya pikir ini berfungsi sekarang dan dapat ditutup?

@jonjaques Btw Anda tidak pernah menggunakan T. Juga, ini tidak menyelesaikan masalah yang saya uraikan yang mengesampingkan metode.

Saya merasa solusi yang disebutkan sudah disarankan hampir 4 tahun yang lalu… Namun itu bukan sesuatu yang menyelesaikan masalah.

Saya membuka stackoverflow untuk melihat apakah ada solusi lain yang mungkin diketahui seseorang dan seseorang memiliki ringkasan yang bagus tentang masalah yang saya alami:

"Generik yang digunakan sebagai parameter metode, termasuk parameter ini, tampaknya kontravarian, tetapi sebenarnya Anda selalu ingin parameter ini diperlakukan sebagai kovarian (atau bahkan mungkin bivarian)."

Jadi sepertinya ini benar-benar sesuatu yang rusak di TypeScript itu sendiri dan harus diperbaiki ('ini' harus kovarian atau bivarian).

EDIT: Komentar ini memiliki cara yang lebih baik.

Saya tidak punya banyak untuk ditambahkan, tetapi ini bekerja dengan baik untuk saya dari 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()

Semoga ini bermanfaat bagi siapa saja yang tidak ingin menelusuri masalah panjang ini.

Kasus penggunaan lain adalah fungsi anggota penjaga tipe khusus pada non kelas:

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

Solusinya hampir membuat saya berada di tempat yang saya inginkan, tetapi saya memiliki satu kunci pas lagi untuk dimasukkan ke dalam persneling. Ambil contoh yang dibuat-buat berikut:

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

Inilah kesalahannya:

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)

Ini masuk akal bagi saya, karena metode create di kelas Automobile tidak memiliki referensi ke ITruckOptions melalui O , tetapi saya ingin tahu apakah ada solusi untuk masalah itu?

Biasanya opsi perluasan kelas saya untuk konstruktor akan memperluas antarmuka opsi kelas dasar, jadi saya tahu mereka akan selalu menyertakan parameter untuk kelas dasar, tetapi tidak memiliki cara yang andal untuk memastikan mereka menyertakan parameter untuk perluasan kelas.

Saya juga harus menggunakan hanya mengganti metode dalam memperluas kelas untuk memberi tahu mereka tentang tipe input dan pengembalian yang diharapkan dari kelas yang mewarisi yang hanya terasa sedikit bau.

@Jack-Barry ini berfungsi:

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

Namun, itu masih tidak ideal karena konstruktor bersifat publik sehingga memungkinkan untuk melakukan new Automobile({ make: "Audi" }) .

@elderapo Saya pikir itu memberi saya apa yang saya butuhkan - contohnya tentu saja dibuat-buat tetapi saya melihat di mana kurangnya pemahaman saya tentang _Generics_ sedikit mengganggu saya. Terima kasih telah membersihkannya!

Semoga ini akan membantu orang lain, beri tahu saya jika ada ruang untuk perbaikan. Itu tidak sempurna, karena fungsi baca berpotensi menjadi tidak sinkron dengan pengetikan tetapi itu yang paling dekat dengan pola yang kami butuhkan.

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

Anda dapat dengan mudah memperluas pola ini ke tindakan CRUD yang tersisa.

Dalam contoh @Jack-Barry, saya mencoba mendapatkan fungsi read untuk mengembalikan instance kelas. Saya mengharapkan yang berikut ini berfungsi:

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

tetapi malah mendapatkan kesalahan Property 'create' does not exist on type 'ThisClass<A, T>'. Adakah yang punya solusi tentang cara mengatasi ini?

@everhardt Karena metode create adalah static itu perlu dipanggil pada class dari this , bukan instance. Yang mengatakan saya tidak benar-benar memiliki solusi yang baik dari atas kepala saya untuk cara mengakses fungsi class secara dinamis yang masih menyediakan pemeriksaan tipe yang diinginkan.

Yang paling dekat yang bisa saya dapatkan tidak terlalu elegan, dan tidak dapat menggunakan type yang ada dari BaseClass.create , sehingga mereka dapat dengan mudah menjadi tidak sinkron:

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

Saya telah _ tidak _ menguji ini.

@Jack-Barry maka Anda mendapatkan this.constructor.create is not a function .

Itu masuk akal: TypeScript menafsirkan this sebagai instance, tetapi untuk parser javascript akhirnya, this adalah kelas karena fungsinya statis.

Mungkin bukan ekstensi yang paling terinspirasi dari contoh Jack-Barry, tetapi di Tautan Playground ini Anda dapat melihat bagaimana saya menyelesaikannya.

intinya:

  • tipe ThisClass harus menjelaskan semua properti dan metode statis yang ingin digunakan oleh metode BaseClass (baik statis maupun instans) dengan polimorfik 'ini'.
  • saat menggunakan properti atau metode statis dalam metode instan, gunakan (this.constructor as ThisClass<A, this>)

Ini pekerjaan ganda, karena Anda harus mendefinisikan jenis metode dan properti statis pada kelas dan tipe ThisClass , tetapi bagi saya itu berfungsi.

edit: perbaiki tautan taman bermain

PHP memperbaiki masalah ini sejak lama. diri sendiri. statis. ini.

Hai semua, Ini tahun 2020, mengalami masalah _ini_. 😛.

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

Saya berharap bugs menjadi turunan dari Bunny .

Apa solusi saat ini? Saya sudah mencoba segalanya dalam masalah ini, sepertinya tidak ada yang diperbaiki.

Pembaruan : Ini berhasil, melewatkan definisi argumen 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

Masalahnya adalah getter dan setter tidak dapat memiliki parameter tipe.

Itu juga terlalu bertele-tele.

Saya berurusan dengan masalah serupa. Untuk menerapkan "kelas dalam polimorfik", yang terbaik yang dapat saya temukan adalah sebagai berikut:

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

Tampaknya berfungsi dengan baik, tetapi pengetikan createInnerClass() menurut saya terlalu bertele-tele, dan saya akan memiliki banyak dari itu di basis kode saya. Adakah yang punya ide tentang cara menyederhanakan kode ini?

Masalah dengan pendekatan itu adalah bahwa itu tidak berfungsi dengan pengambil dan penyetel statis.

Bagaimana menerapkan self. untuk properti statis? Saya memiliki masalah ini 4 tahun yang lalu, dan saya kembali ke utas yang sama dengan masalah yang sama.

@sudomaxime Itu di luar cakupan masalah ini dan berjalan bertentangan dengan tujuan desain TypeScript selama ECMAScript tidak secara asli mendukung self. sebagai alias untuk this.constructor. di kelas, yang tidak mungkin terjadi mengingat self adalah nama variabel yang umum.

Solusi @abdullah-kasim tampaknya berfungsi seperti yang dijelaskan, tetapi saya tidak dapat membuatnya berfungsi dengan kelas generik:

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

Saya akan menghargai pemikiran apa pun tentang apakah ini layak karena saya telah mengutak-atiknya sebentar tanpa hasil.

@stevehanson Apakah ini akan berhasil untuk Anda?

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

Menguji ini di taman bermain TypeScript.

Terinspirasi dari tautan ini:
https://selleo.com/til/posts/gll9bsvjcj-generic-with-class-as-argument-and-returning-instance

Dan saya tidak yakin bagaimana itu berhasil menyiratkan bahwa T adalah kelas itu sendiri . EDIT: Ah, saya ingat, itu berhasil menyiratkan T karena parameter silent this akan selalu melewati kelas itu sendiri.

@abdullah-kasim ini berhasil! Wow Terimakasih! Untuk kasus khusus saya, konstruktor saya mengambil argumen, jadi seperti ini bagi saya, jika itu membantu orang lain:

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

Beberapa solusi di sini tidak berfungsi untuk pengambil statis karena saya tidak dapat memberikan argumen apa pun di:

Diberikan contoh di bawah ini, bagaimana saya bisa memindahkan getter statis default ke kelas induk?

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

const x = Letters.default.alpha;

Ini mungkin sesuatu yang layak dipertimbangkan jika kita pernah mempertimbangkan perbaikan sintaks.

Mungkin terkait: Saya mengalami kesulitan menambahkan lapisan abstraksi lain ke contoh @abdullah-kasim. Pada dasarnya, saya ingin dapat mengubah Animal menjadi sebuah antarmuka, dan mengizinkan Bunny untuk mendefinisikan sendiri static create() .

Inilah contoh praktisnya (maafkan saya karena mengabaikan analogi Kelinci!). Saya ingin dapat mendefinisikan beberapa jenis dokumen yang berbeda, dan masing-masing harus dapat mendefinisikan metode pabrik statis yang mengubah string menjadi instance dokumen. Saya ingin menegakkan bahwa jenis dokumen hanya boleh mendefinisikan parser untuk dirinya sendiri, dan bukan untuk jenis dokumen lain.

(tipe pada contoh di bawah ini kurang tepat - semoga cukup jelas apa yang saya coba capai)

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

(Saya pikir) saya ingin dapat menulis sesuatu seperti ini:

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

Adakah ide apakah solusi juga mungkin dilakukan di sini?

EDIT: Kemungkinan Solusi

Kasus penggunaan lain di StackOverflow
Untuk merancang kelas tabel pencarian generik.

// 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 berkomentar pada 29 Mei 2016
Tidak percaya ini masih belum diperbaiki / ditambahkan.....

ha ha
2020 telah tiba!

Ini konyol dan gila. Ini tahun 2020. 4 tahun kemudian, mengapa ini tidak diperbaiki?

Mengapa tidak menerapkan sesuatu seperti

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
Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

siddjain picture siddjain  ·  3Komentar

fwanicka picture fwanicka  ·  3Komentar

MartynasZilinskas picture MartynasZilinskas  ·  3Komentar

bgrieder picture bgrieder  ·  3Komentar

dlaberge picture dlaberge  ·  3Komentar