Typescript: متعدد الأشكال "هذا" للأعضاء الثابتة

تم إنشاؤها على ١ ديسمبر ٢٠١٥  ·  150تعليقات  ·  مصدر: microsoft/TypeScript

عند محاولة تنفيذ نظام نموذج سجل نشط أساسي إلى حد ما ولكنه متعدد الأشكال ، نواجه مشكلات مع نظام النوع الذي لا يحترم this عند استخدامه مع المُنشئ أو القالب / عام.

لقد نشرت من قبل حول هذا هنا ، يبدو أن # 5493 و # 5492 يشير إلى هذا السلوك أيضًا.

وهنا منشور SO هذا الذي قمت به:
http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved

لقد قمت بإعادة تدوير المثال الخاص بي من # 5493 في هذه التذكرة لمزيد من المناقشة. أردت بطاقة مفتوحة تمثل الرغبة في شيء من هذا القبيل والمناقشة ولكن الاثنان الآخران مغلقان.

هذا مثال يوضح نموذج Factory الذي ينتج النماذج. إذا كنت ترغب في تخصيص BaseModel الذي يعود من Factory ، فيجب أن تكون قادرًا على تجاوزه. لكن هذا فشل لأنه لا يمكن استخدام this في عضو ثابت.

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

ربما تكون هذه المشكلة سخيفة وهناك حل سهل. أرحب بالتعليقات المتعلقة بكيفية تحقيق مثل هذا النمط.

In Discussion Suggestion

التعليق الأكثر فائدة

هل هناك سبب لإغلاق هذه القضية؟

حقيقة أن هذا متعدد الأشكال لا يعمل على الإحصائيات بشكل أساسي يجعل هذه الميزة DOA ، في رأيي. حتى الآن لم أحتاج في الواقع إلى تعدد الأشكال هذا على أعضاء المثيل ، ومع ذلك كنت بحاجة إلى كل بضعة أسابيع على الإحصائيات ، حيث تم الانتهاء من نظام التعامل مع الإحصائيات في الأيام الأولى. شعرت بسعادة غامرة عندما تم الإعلان عن هذه الميزة ، ثم خذلتها لاحقًا عندما أدركت أنها تعمل فقط على أعضاء المثيل.

حالة الاستخدام أساسية جدًا وشائعة جدًا. ضع في اعتبارك طريقة مصنع بسيطة:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

في هذه المرحلة ، كنت إما ألجأ إلى أسلوب الصب القبيح أو استخدام طرق static create() في كل وريث. يبدو عدم وجود هذه الميزة وكأنه فجوة اكتمال كبيرة إلى حد ما في اللغة.

ال 150 كومينتر

حجم وشكل قاربي مشابه تمامًا! أهوي!

ترجع طريقة المصنع في الطبقة الفائقة مثيلات جديدة من الفئات الفرعية. تعمل وظيفة الكود الخاص بي ولكنها تتطلب مني إرسال نوع الإرجاع:

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.

واجهت هذه القضية اليوم أيضا!

يتمثل حل الإصلاح في تمرير النوع الفرعي كنوع عام لتمديد الفئة الأساسية ، وهو حل أتقدم بطلبه في الوقت الحالي:

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

هل هناك سبب لإغلاق هذه القضية؟

حقيقة أن هذا متعدد الأشكال لا يعمل على الإحصائيات بشكل أساسي يجعل هذه الميزة DOA ، في رأيي. حتى الآن لم أحتاج في الواقع إلى تعدد الأشكال هذا على أعضاء المثيل ، ومع ذلك كنت بحاجة إلى كل بضعة أسابيع على الإحصائيات ، حيث تم الانتهاء من نظام التعامل مع الإحصائيات في الأيام الأولى. شعرت بسعادة غامرة عندما تم الإعلان عن هذه الميزة ، ثم خذلتها لاحقًا عندما أدركت أنها تعمل فقط على أعضاء المثيل.

حالة الاستخدام أساسية جدًا وشائعة جدًا. ضع في اعتبارك طريقة مصنع بسيطة:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

في هذه المرحلة ، كنت إما ألجأ إلى أسلوب الصب القبيح أو استخدام طرق static create() في كل وريث. يبدو عدم وجود هذه الميزة وكأنه فجوة اكتمال كبيرة إلى حد ما في اللغة.

@ paul-go القضية ليست مغلقة ...؟

@ paul-go لقد شعرت بالإحباط من هذه المشكلة أيضًا ولكن ما يلي هو الحل الأكثر موثوقية الذي وجدته. ستحتاج كل فئة فرعية من الحيوانات إلى استدعاء super.create () وإلقاء النتيجة على نوعها. ليست مشكلة كبيرة وهي عبارة عن بطانة واحدة يمكن إزالتها بسهولة باستخدام هذا.

المترجم ، التحسس ، والأهم من ذلك الأرنب ، كلهم ​​سعداء.

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 عفوًا ... لسبب ما خلطت بين هذا و # 5862 ... آسف لعدوان فأس المعركة :-)

@ Think7 نعم ... ومن هنا جاء "اللجوء إلى أسلوب الصب القبيح أو التخلص من الفضلات الثابتة () في كل وريث". من الصعب جدًا على الرغم من أنك مطور مكتبة ولا يمكنك حقًا إجبار المستخدمين النهائيين على تنفيذ مجموعة من الأساليب الثابتة المكتوبة في الفئات التي يرثونها منك.

لول. غاب تماما عن كل شيء تحت الرمز الخاص بك: د

مه كان يستحق كل هذا العناء ، حصلت على رسم أرنب.

: +1: أرنب

: أرنب:: قلب:

+1 ، بالتأكيد ترغب في رؤية هذا

هل كان هناك أي تحديثات مناقشة حول هذا الموضوع؟

لا يزال على تراكم اقتراحاتنا الهائلة.

جافا سكريبت تعمل بالفعل بشكل صحيح في مثل هذا النمط. إذا كان من الممكن أن يتبع TS أيضًا ، فسيوفر لنا الكثير من التعليمات البرمجية المعيارية / الإضافية. "نموذج النموذج" هو نمط قياسي جدًا ، أتوقع أن يعمل TS كما يفعل JS في هذا الشأن.

أود أيضًا حقًا أن أحب هذه الميزة لنفس أسباب "نموذج CRUD" مثل أي شخص آخر. أحتاجه على الطرق الثابتة أكثر من طرق المثيل.

سيوفر هذا حلاً أنيقًا للمشكلة الموضحة في # 8164.

من الجيد أن هناك "حلول" مع التجاوزات والأدوية ، لكنها لا تحل أي شيء هنا حقًا - الغرض الكامل من وجود هذه الميزة هو تجنب مثل هذه التجاوزات / الإرسال وتحقيق التناسق مع كيفية إرجاع this يتم التعامل مع النوع في طرق المثيل.

أنا أعمل على كتابة Sequelize 4.0 وهو يستخدم أسلوبًا حيث تقوم بتصنيف فئة فرعية من فئة Model . تحتوي فئة النموذج هذه على طرق ثابتة لا حصر لها مثل findById() وما إلى ذلك بالطبع لا تُرجع Model لكن الفئة الفرعية الخاصة بك ، المعروفة أيضًا باسم this في السياق الثابت:

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

هذا غير ممكن للكتابة حاليا. Sequelize هو _the_ ORM لـ Node ومن المحزن أنه لا يمكن كتابته. حقا بحاجة لهذه الميزة. الطريقة الوحيدة هي الإرسال في كل مرة تقوم فيها باستدعاء إحدى هذه الوظائف أو تجاوز كل واحدة منها ، وتكييف نوع الإرجاع ولا تفعل شيئًا سوى استدعاء super.method() .

ومن الأمور ذات الصلة أيضًا أن الأعضاء الثابتون لا يمكنهم الإشارة إلى وسيطات النوع العام - تأخذ بعض الأساليب كائنًا حرفيًا لسمات النموذج التي يمكن كتابتها من خلال وسيطة النوع ، ولكنها متاحة فقط لأعضاء المثيل.

😰 لا أصدق أن هذا لم يتم إصلاحه / إضافته ...

يمكننا الاستفادة من هذا:

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

declare class UIButton extends NSObject {
}

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

هذه حالة استخدام أتمنى أن أعملها (تم ترحيلها من https://github.com/Microsoft/TypeScript/issues/9775 التي أغلقتها لصالح هذا)

لا يمكن حاليًا استخدام معلمات المُنشئ this اكتب فيها:

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

متوقع: يكون هذا متاحًا لمعلمات المُنشئ.

مشكلة مطلوب حلها:

  • أن تكون قادرًا على بناء واجهة برمجة التطبيقات الخاصة بي بطلاقة إما حول نفسها حول واجهة برمجة تطبيقات أخرى بطلاقة تعيد استخدام نفس الكود:
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();

الحل البديل المستقبلي:

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

بهذه الطريقة ، عندما يدعم TypeScript this للأعضاء الساكنين ، سيكون رمز المستخدم الجديد هو Baz.create() بدلاً من Baz.create<Baz>() بينما سيعمل رمز المستخدم القديم بشكل جيد. :ابتسامة:

هذا حقا مطلوب! خاصة بالنسبة لـ DAO التي تحتوي على أساليب ثابتة تقوم بإرجاع المثيل. تم تعريف معظمها على قاعدة DAO ، مثل (حفظ ، الحصول ، تحديث ، إلخ ...)

أستطيع أن أقول أنه قد يسبب بعض الالتباس ، فوجود نوع this على طريقة ثابتة يتم حله إلى الفصل الذي ينتمي إليه وليس نوع الفصل (على سبيل المثال: typeof ).

في JS الحقيقية ، سيؤدي استدعاء طريقة ثابتة على نوع ما إلى أن يكون this داخل الوظيفة هو الفئة وليس مثيلًا لها ... لذا فهي غير متسقة.

ومع ذلك ، في حدس الناس ، أعتقد أن أول شيء ينبثق عند رؤية نوع إرجاع this على طريقة ثابتة هو نوع المثيل ...

shlomiassaf لن يكون غير متناسق. عندما تحدد فئة كنوع إرجاع لوظيفة مثل المستخدم ، سيكون نوع الإرجاع مثيلًا للمستخدم. بالضبط نفس الشيء ، عندما تحدد نوع إرجاع من هذا على طريقة ثابتة ، سيكون نوع الإرجاع نسخة من هذا (مثيل للفئة). يمكن بعد ذلك نمذجة الطريقة التي تُرجع الفئة نفسها باستخدام typeof this.

felixfbecker هذا شيء وجهة نظر تمامًا ، هكذا تختار أن تنظر إليه.

دعونا نفحص ما يحدث في JS ، حتى نتمكن من استنتاج المنطق:

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

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

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

الآن ، this في وقت التشغيل الحقيقي هو السياق المحدود للوظيفة ، وهذا بالضبط ما يحدث في واجهات API بطلاقة ، وتساعد هذه الميزة متعددة الأشكال عن طريق إرجاع النوع الصحيح ، والذي يتماشى فقط مع كيفية عمل JS ، تُرجع الفئة الأساسية this المثيل الذي تم إنشاؤه بواسطة الفئة المشتقة. الميزة هي في الواقع إصلاح.

لنلخص:
من المتوقع أن تقوم الطريقة التي تُرجع this والتي تم تعريفها كجزء من النموذج الأولي (مثيل) بإرجاع المثيل.

استمرارًا لهذا المنطق ، من المتوقع أن تؤدي الطريقة التي تُرجع this والتي تم تعريفها كجزء من نوع الفئة (النموذج الأولي) إلى سياق محدد ، وهو نوع الفئة.

مرة أخرى ، لا تحيز ، لا رأي ، حقائق بسيطة.

حدسًا ، سأشعر بالراحة عند إرجاع this من دالة ثابتة تمثل المثيل نظرًا لأنه تم تعريفه داخل النوع ، ولكن هذا أنا. قد يفكر الآخرون بشكل مختلف ولا يمكننا إخبارهم بخطئهم.

تكمن المشكلة في أنه يجب أن يكون من الممكن كتابة كل من طريقة ثابتة تقوم بإرجاع مثيل فئة وطريقة ثابتة تقوم بإرجاع الفئة نفسها (نوع هذا). منطقك منطقي من منظور JS ، لكننا نتحدث عن عودة أنواع _ هنا ، واستخدام فئة كنوع إرجاع (هنا هذا) في TypeScript وأي لغة أخرى يعني دائمًا مثيل الفئة. لإرجاع الفئة الفعلية ، لدينا عامل التشغيل typeof.

felixfbecker هذا يثير قضية أخرى!

إذا كان النوع this هو نوع المثيل ، فإنه يختلف عما تشير إليه الكلمة الأساسية this في نص الطريقة الثابتة. ينتج عن return this نوع إرجاع دالة typeof this ، وهو أمر غريب تمامًا!

لا ليس كذلك. عندما تحدد طريقة مثل

getUser(): User {
  ...
}

تتوقع استعادة User _instance_ ، وليس فئة المستخدم (هذا ما يمثله typeof ). إنها طريقة عملها في كل لغة مكتوبة. تختلف دلالات النوع ببساطة عن دلالات وقت التشغيل.

لماذا لا تستخدم الكلمات الرئيسية this أو static كمُنشئ في func للتلاعب بفئة فرعية؟

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

وإذا قارنا هذا بمثال في JS ، فسنرى ما يعمل بشكل صحيح:

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

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

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

لا يمكنك استخدام النوع this على طريقة ثابتة ، أعتقد أن هذا هو جذر المشكلة بالكامل.

تضمين التغريدة

ضع في اعتبارك هذا:

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

يعد هذا النوع من التعليقات التوضيحية بديهيًا ، ولكنه غير صحيح إذا كان النوع this في أسلوب ثابت هو مثيل الفئة. الكلمة الرئيسية this لها نوع typeof this بطريقة ثابتة!

أشعر أن this _should_ يشير إلى نوع المثيل ، وليس نوع الفئة ، لأنه يمكننا الحصول على مرجع لنوع الفئة من نوع المثيل ( typeof X ) ولكن ليس العكس ( instancetypeof X ؟)

xealot حسنًا ، لماذا لا تستخدم الكلمة الرئيسية static بدلاً this ؟ لكن this في سياق JS الثابت لا يزال يشير إلى المُنشئ.

izatop نعم ، يعمل جافا سكريبت الذي تم إنشاؤه (بشكل صحيح أو غير صحيح). ومع ذلك ، هذا ليس عن جافا سكريبت. الشكوى ليست أن لغة Javascript لا تسمح بهذا النمط ، بل أن نظام كتابة Typescript لا يسمح بذلك.

لا يمكنك الحفاظ على أمان النوع باستخدام هذا النمط لأن Typescript لا تسمح بأنواع this متعددة الأشكال على الأعضاء الثابتة. يتضمن ذلك طرق الفصل المحددة باستخدام الكلمة الرئيسية static و constructor .

رابط الملعب

LPGhatguy لكنك قرأت تعليقي السابق ، أليس كذلك؟! إذا كانت لديك طريقة من نوع الإرجاع User ، فإنك تتوقع return new User(); ، وليس return User; . بنفس الطريقة يجب أن تعني القيمة المرجعة this return new this(); بطريقة ثابتة. يختلف هذا في الطرق غير الثابتة ، لكنه واضح لأن this هو كائن دائمًا ، فلن تتمكن من إنشاء مثيل له. لكن في النهاية ، نتفق جميعًا بشكل أساسي على أن هذا هو أفضل بناء جملة بسبب typeof وأن هذه الميزة مطلوبة بشدة في TS.

xealot أنا أفهم ما الذي لا يسمح به TypeScript متعدد الأشكال this ، فكم سأطلب منك لماذا لا تضيف هذه الميزة إلى TS؟

لا أعرف ما إذا كان ما يلي سيعمل مع جميع حالات الاستخدام لهذه المشكلة ، لكنني اكتشفت أن استخدام النوع this: في الأساليب الثابتة جنبًا إلى جنب مع الاستخدام الذكي لنظام الاستدلال النوع يسمح بالمتعة الكتابة. قد لا يكون مقروءًا بشكل لا يصدق ، لكنه يؤدي المهمة دون الحاجة إلى إعادة تعريف الأساليب الثابتة في فصول الأطفال.

يعمل مع [email protected]

ضع في اعتبارك ما يلي ؛

// 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 لماذا يتم إعطاء this كمعامل؟

يرجع هذا إلى https://github.com/Microsoft/TypeScript/issues/3694 الذي تم شحنه مع الإصدار 2.0.0 من النوع الذي يسمح بتعريف هذه المعلمة للوظائف (مما يسمح باستخدام this في أجسامهم بدون وكذلك منع الاستخدام غير الصحيح للوظائف)

اتضح أنه يمكننا استخدامها في الطرق بالإضافة إلى الطريقة الثابتة ، وأنها تسمح بأشياء مضحكة حقًا ، مثل استخدامها لـ "مساعدة" المستنتج من خلال إجباره على مقارنة this الذي نقدمه (IModel) إلى الشخص المستخدم (كلب أو قطة). ثم "يخمن" النوع الصحيح لـ T ويستخدمه ، وبالتالي يكتب وظيفتنا بشكل صحيح.

(لاحظ أن this هي معلمة _ خاصة_ يفهمها المترجم المنسوخ ، ولا تضيف واحدة أخرى إلى الوظيفة)

اعتقدت أن بناء الجملة يشبه <this extends Whatever> . إذن ، سيستمر هذا الأمر في العمل إذا كان لدى create() معلمات أخرى؟

قطعا

أردت أيضًا أن أشير إلى أن هذا مهم لأنواع البناء.
عندما تقوم بالفئة الفرعية Array على سبيل المثال ، فإن طريقة from() الخاصة بالفئة الفرعية ستعيد مثيلاً من الفئة الفرعية ، وليس 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

أي تعديل حدث في هذه القضية؟ ستعمل هذه الميزة على تحسين تجربة فريقي بشكل كبير مع TypeScript.

يرجى ملاحظة أن هذا أمر بالغ الأهمية نظرًا لأنه نمط شائع الاستخدام في ORMs والكثير من التعليمات البرمجية تتصرف بشكل مختلف تمامًا عن نظام التحويل البرمجي والنوع.

أود أن أثير بعض الحجج المضادة ضد الحجة المضادة.

كما أشار shlomiassaf ، فإن السماح بدفع أعضاء ثابتة this سيتسبب في حدوث ارتباك لأن this يعني شيئًا مختلفًا في الطريقة الثابتة وطريقة المثيل. علاوة على ذلك ، فإن التعليق التوضيحي من النوع this و this في نص الأسلوب لهما دلالات مختلفة. يمكننا تجنب هذا الالتباس عن طريق إدخال كلمة رئيسية جديدة ، ولكن تكلفة كلمة رئيسية جديدة مرتفعة.

وهناك حل سهل في TypeScript الحالية. ceymard قد أظهر ذلك بالفعل. وأنا أفضل منهجه لأنه يلتقط دلالات وقت التشغيل للطريقة الثابتة. ضع في اعتبارك هذا الرمز.

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

إذا كان هذا متعدد الأشكال مدعومًا ، يجب أن يبلغ المترجم عن خطأ في class B extends A . لكن هذا تغيير جذري.

HerringtonDarkholme لا أرى الالتباس ، نوع الإرجاع this هو بالضبط ما أتوقعه من return new this() . من المحتمل أن نستخدم self لذلك ، لكن تقديم كلمة مفتاحية أخرى (أخرى غير تلك المستخدمة لإنتاج المخرجات) يبدو أكثر إرباكًا بالنسبة لي. نحن نستخدمه بالفعل كنوع إرجاع ، المشكلة الوحيدة (لكن الكبيرة) هي أنها غير مدعومة في الأعضاء الثابتة.

لا ينبغي أن يطلب المترجم من المبرمجين إجراء حلول بديلة ، فمن المفترض أن يكون TypeScript "JavaScript محسّنًا" وهذا ليس صحيحًا في هذه الحالة ، عليك القيام بحل بديل معقد للكود الخاص بك حتى لا ينتج عنه ملايين الأخطاء. من الجيد أن نتمكن من تحقيقه في الإصدار الحالي بطريقة ما ، لكن هذا لا يعني أنه جيد ولا يحتاج إلى إصلاح.

لماذا يختلف نوع الإرجاع this عن نوع this في نص الطريقة ، إذن؟
لماذا فشل هذا الرمز؟ كيف تشرحها للقادم الجديد؟

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

لماذا قد تفشل مجموعة شاملة من JavaScript في قبول هذا؟

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

abstract class B extends A {}

صحيح ، قد نحتاج إلى تقديم كلمة رئيسية أخرى بعد ذلك. يمكن أن يكون self ، أو ربما instance

HerringtonDarkholme هنا كيف أشرحها للوافد الجديد.
عندما تفعل

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

نوع الإرجاع B يعني أنك بحاجة إلى إرجاع مثيل B . إذا كنت تريد إرجاع الفصل نفسه ، فأنت بحاجة إلى استخدامه

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

الآن ، تمامًا كما هو الحال في JavaScript ، يشير أعضاء ثابت this إلى الفصل الدراسي. لذلك إذا كنت تستخدم نوع إرجاع this في طريقة ثابتة ، فسيتعين عليك إرجاع مثيل للفئة:

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

إذا كنت تريد إرجاع الفصل نفسه ، فأنت بحاجة إلى استخدام typeof this :

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

هذا هو بالضبط كيف تعمل الأنواع بالفعل في TS منذ ذلك الحين. إذا قمت بالكتابة باستخدام فئة ، فإن TS تتوقع مثيلاً. إذا كنت تريد الكتابة لنوع الفصل نفسه ، فأنت بحاجة إلى استخدام typeof .

ثم أسأل لماذا لا يحمل this نفس المعنى في نص الطريقة. إذا كان التعليق التوضيحي لنوع أسلوب المثيل this له نفس المعنى مع this في نص الأسلوب المقابل ، فلماذا لا يكون هو نفسه بالنسبة للطريقة الثابتة؟

نحن أمام معضلة وهناك ثلاثة خيارات:

  1. اجعل this تعني أشياء مختلفة في سياق مختلف (ارتباك)
  2. تقديم كلمة رئيسية جديدة مثل self أو static
  3. دع المبرمج يكتب المزيد من التعليقات التوضيحية ذات النوع الغريب (تؤثر على بعض المستخدمين)

أفضل الطريقة الثالثة لأنها تعكس دلالات وقت التشغيل لتعريف الطريقة الثابتة .
كما أنه يكتشف الأخطاء في استخدام الموقع ، بدلاً من تحديد الموقع ، أي ، كما في هذا التعليق ، تم الإبلاغ عن الخطأ في B.create ، وليس class B extends A . استخدام خطأ الموقع هو أكثر دقة في هذه الحالة. (ضع في اعتبارك أنك تعلن عن طريقة ثابتة تشير إلى this ثم تعلن عن فئة فرعية مجردة).

والأهم من ذلك ، أنها لا تتطلب اقتراح لغة لميزة جديدة.

في الواقع يختلف التفضيل عن الأشخاص. لكن على الأقل أريد أن أرى اقتراحًا أكثر تفصيلاً مثل هذا . هناك الكثير من المشكلات التي يجب حلها مثل إمكانية تعيين الفئة المجردة وتوقيعات المُنشئ غير المتوافقة للفئة الفرعية واستراتيجيات تقرير الخطأ.

HerringtonDarkholme لأنه في حالة عمليات المثيل ، وقت التشغيل this هو مثيل كائن وليس فئة ، لذلك لا يوجد مساحة للارتباك. من الواضح أن النوع this هو حرفياً وقت التشغيل this ، ولا يوجد خيار آخر. بينما في الحالة الثابتة ، يتصرف مثل أي تعليق توضيحي آخر للفئة في أي لغة برمجة موجهة للكائنات: قم بالتعليق عليها بفصل ، وتتوقع عودة مثيل. بالإضافة إلى ذلك ، في حالة TS ، لأن فئات JavaScript عبارة عن كائنات أيضًا ، اكتبها بـ typeof للفئة وستستعيد الفئة الحرفية.

أعني أنه كان من المحتمل أن يكون أكثر اتساقًا عندما قاموا في ذلك الوقت بتطبيق نوع الإرجاع this للتفكير أيضًا في الحالة الثابتة وبدلاً من ذلك يطلب من المستخدمين كتابة typeof this دائمًا لطرق المثال. لكن هذا القرار تم اتخاذه في ذلك الوقت ، لذا يجب علينا العمل معه الآن ، وكما قلت ، لا يترك أي مجال للارتباك لأن طرق المثال this لا تنتج أي نوع آخر (على عكس الفئات ، من لها نوع ثابت ونوع مثيل) ، لذلك فهي مميزة في هذه الحالة ولن تربك أي شخص.

حسنًا ، لنفترض فقط أنه لن يتم الخلط بين أي شخص this . ماذا عن تعيين فئة مجردة؟

ثبات الأساليب تعسفي إلى حد ما. يمكن أن يكون لأي وظيفة خصائص وطرق. إذا كان من الممكن استدعاؤها أيضًا بـ new ، فإننا نختار استدعاء هذه الخصائص والطرق static . الآن تم ترسيخ هذه الاتفاقية بقوة في ECMAScript.

HerringtonDarkholme ، أنت بلا شك محق في أن استخدام this قد يسبب التباسًا. ومع ذلك ، نظرًا لأنه لا يوجد شيء غير صحيح حول استخدام this ، أود أن أقول إنه جيد تمامًا ، خاصة عند الأخذ في الاعتبار تحليلك الذكي للتكلفة والمزايا للبدائل المختلفة. أعتقد أن this هو أقل البدائل سوءًا.

لقد حاولت للتو اللحاق بهذا الموضوع من مشاركتي الأصلية ، أشعر أن هذا ربما يكون قد انحرف قليلاً.

للتوضيح ، الرغبة في أن يكون التعليق التوضيحي من النوع this متعدد الأشكال عند استخدامه في المُنشئ ، على وجه التحديد كنموذج عام / قالب. فيما يتعلق بـ shlomiassaf و HerringtonDarkholme ، أعتقد أن المثال باستخدام أساليب static يمكن أن يكون مربكًا وليس الغرض من هذه المشكلة.

على الرغم من أنه لا يتم التفكير في الأمر على هذا النحو غالبًا ، إلا أن منشئ الفصل ثابت. المثال (الذي سأعيد نشره مع المزيد من التعليقات التوضيحية) لا يعلن this على طريقة ثابتة ، إنه بدلاً من ذلك يعلن this للاستخدام المستقبلي عبر عامة في نوع التعليق التوضيحي للطريقة الثابتة .

الفرق هو أنني لا أريد حساب this على الفور بطريقة ثابتة ، ولكن في المستقبل بطريقة ديناميكية.

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

إلى أي شخص يعتقد أن هذا أمر محير ، حسنًا ، هذا ليس بالأمر الصريح تمامًا. ومع ذلك ، فإن استخدام this في مُنشئ BaseModel ليس في الواقع استخدامًا ثابتًا ، بل هو استخدام مؤجل ليتم حسابه لاحقًا. ومع ذلك ، فإن الطرق الثابتة (بما في ذلك المنشئ) لا تعمل بهذه الطريقة.

أعتقد أنه في وقت التشغيل كل هذا يعمل كما هو متوقع. ومع ذلك ، فإن نظام النوع غير قادر على نمذجة هذا النمط المعين. عدم القدرة على الكتابة المطبوعة لنمذجة نمط جافا سكريبت هي أساس سبب فتح هذه المشكلة.

آسف إذا كان هذا تعليق محير ، كتب على عجل.

xealot لقد فهمت وجهة نظرك ، لكن المشكلات الأخرى التي اقترحت على وجه التحديد this متعدد الأشكال للطرق الثابتة تم إغلاقها كنسخة مكررة من هذه المشكلة. أفترض أن الإصلاح في TS سيمكن كلا حالتي الاستخدام.

ما هي حالة الاستخدام الخاصة بك بالضبط؟ ربما يكفي هذا الحل.

أستخدمها في مكتبة ORM مخصصة بنجاح
لو جو. 20 أكتوبر 2016 الساعة 19:15 ، Tom Marius [email protected] أ
écrit:

يرجى ملاحظة أن هذا أمر بالغ الأهمية لأنه شائع الاستخدام
نمط في ORMs والكثير من التعليمات البرمجية يتصرف بشكل مختلف تمامًا عن
المترجم ونوع النظام يعتقد أنه ينبغي.

-
أنت تتلقى هذا لأنه تم ذكرك.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -255169194 ،
أو كتم الخيط
https://github.com/notifications/unsubscribe-auth/AAtAoRHc45B386xdNhuCLB8iW6i82e7Uks5q16GzgaJpZM4GsmOH
.

بفضل ceymard وHerringtonDarkholme.

static create<T extends A>(this: {new (): T}) { return new this(); } فعل الحيلة بالنسبة لي 👍

إنه أكثر غموضًا من static create(): this { return new this(); } للوهلة الأولى ولكنه على الأقل صحيح! إنها تلتقط بدقة نوع this داخل الطريقة الثابتة.

آمل أن يتم إعطاء الأولوية لهذا بطريقة ما ، على الرغم من أنه ليس الأكثر تأييدًا أو تعليقًا وما إلى ذلك. إنه أمر مزعج للغاية أن تضطر إلى استخدام وسيطة الكتابة للحصول على النوع الصحيح ، على سبيل المثال:

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

عندما يكون من الممكن عمليًا القيام بما يلي:

let u = User.create();

ومن المحتم أن يشير الثابت أحيانًا إلى أنواع المثيلات.

كانت لدي فكرة أخرى بخصوص المناقشة حول ما إذا كان النوع this في الأعضاء الثابت يجب أن يكون نوع الجانب الثابت للفصل أو جانب المثيل. يمكن أن يكون this هو الجانب الثابت ، ويتطابق مع ما يتم في أعضاء المثيل وما هو سلوك وقت التشغيل ، ومن ثم يمكنك الحصول على نوع المثيل من خلال this.prototype :

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

class User extends Model {}

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

يتماشى هذا مع كيفية عمله في JavaScript. this هو كائن الفئة نفسه ، والمثيل موجود على this.prototype . إنه في الأساس نفس ميكانيكي الأنواع المعينة ، أي ما يعادل this['prototype'] .

بالطريقة نفسها ، يمكنك تخيل الجانب الثابت ليكون متاحًا في أعضاء مثيل من خلال this.constructor (لا يمكنني التفكير في حالة استخدام لهذا الصراف الآلي).

أردت أيضًا أن أذكر أن أيًا من الحلول / الاختراقات المذكورة لا تعمل لكتابة نموذج ORM في Sequelize.

  • لا يمكن استخدام التعليق التوضيحي من النوع this في المُنشئ ، لذا فهو لا يعمل مع هذا التوقيع:
    ts abstract class Model { constructor(values: Partial<this.prototype>) }
  • لا يعمل تمرير النوع كوسيطة فئة عامة مع الأساليب الثابتة ، حيث لا يمكن للأعضاء الثابت الإشارة إلى المعلمات العامة
  • تمرير النوع كوسيطة طريقة عامة يعمل مع الطرق ، لكنه لا يعمل مع الخصائص الثابتة ، مثل:
    ts abstract class Model { static attributes: { [K in keyof this.prototype]: { type: DataType, field: string, unique: boolean, primaryKey: boolean, autoIncrement: boolean } }; }

+1 لهذا الطلب. سيكون ORM الخاص بي نظيفًا جدًا.

نأمل أن نرى الحل النظيف قريبًا

في غضون ذلك ، شكرا لجميع الذين قدموا الحلول!

هل يمكننا المضي قدمًا في هذا الأمر نظرًا لعدم وجود مزيد من المناقشة؟

إلى أين تريد أن تأخذه؟ لا تزال هذه طريقة لا يمكن بها لـ Typescript تصميم Javascript بشكل واضح بقدر ما أعرف ولم أشاهد أي عمل بخلاف عدم القيام بذلك.

هذه ميزة لا غنى عنها لمطوري ORM ، حيث يمكننا أن نرى بالفعل ثلاثة مالكي ORM مشهورين طلبوا هذه الميزة.

تم اقتراح حلولpleerockxealot أعلاه :

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

أنا أستخدم هذا على نطاق واسع. إعلانات الطرق الثابتة في الفئات الأساسية ثقيلة بعض الشيء ولكن هذا هو الثمن الذي يجب دفعه لتجنب التشويه على نوع this داخل الطرق الثابتة.

تضمين التغريدة

لديّ ORM داخلي يستخدم نمط this: على نطاق واسع دون مشاكل.

أعتقد أنه ليست هناك حاجة إلى زيادة تكلفة اللغة عندما تكون الميزة موجودة بالفعل هنا بالفعل وإن كانت معقدة بعض الشيء. حالة الاستخدام لبناء جملة أكثر وضوحًا محدودة جدًا ويمكن أن تؤدي إلى تناقضات.

ربما يمكن أن يكون هناك موضوع Stackoverflow مع هذا السؤال والحلول للرجوع إليها؟

bjouhierceymard شرحت لماذا لا تعمل جميع الحلول في هذا الموضوع مع Sequelize: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -269463313

وهذا لا يتعلق فقط بـ ORM ، ولكن أيضًا بالمكتبة القياسية: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -244550725

felixfbecker نشرت للتو شيئًا عن حالة استخدام المُنشئ وحذفتها (أدركت أن TS لا يفرض على المُنشئين في كل فئة فرعية بعد النشر مباشرةً).

felixbecker فيما يتعلق بحالة المُنشئ ، يمكنني حلها من خلال التمسك بالمنشئات بدون معلمات وتوفير طرق ثابتة متخصصة بدلاً من ذلك (إنشاء ، استنساخ ، ...). أكثر وضوحا وأسهل في الكتابة.

bjouhier قد يعني ذلك أنه يتعين عليك إعادة تعريف كل طريقة بشكل أساسي في كل فئة فرعية من النماذج. انظر كم يوجد في Sequelize: http://docs.sequelizejs.com/class/lib/model.js~Model.html

حجتي من منظور مختلف تماما. في حين أن هناك حلول جزئية وغير بديهية إلى حد ما ويمكننا الجدال والاختلاف حول ما إذا كانت كافية أم لا ، ما يمكننا الاتفاق على أن هذا مجال لا يمكن فيه لـ Typescript تصميم جافا سكريبت بسهولة.

وما أفهمه هو أن تنسكريبت يجب أن تكون امتدادًا لجافا سكريبت.

تضمين التغريدة

قد يعني ذلك أنه يتعين عليك إعادة الإعلان بشكل أساسي عن كل طريقة مفردة في كل فئة فرعية من النماذج.

لماذا ا؟ هذه ليست تجربتي على الإطلاق. هل يمكنك توضيح المشكلة باستخدام نموذج التعليمات البرمجية؟

bjouhier لقد أوضحت المشكلة بعمق مع نماذج التعليمات البرمجية في تعليقاتي في هذا الموضوع ، للبدء https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -222348054

لكن انظر إلى مثال create الخاص بي أعلاه . طريقة create طريقة ثابتة. لم يتم إعادة تعريفه ومع ذلك تم كتابته بشكل صحيح في الفئة الفرعية. لماذا تحتاج أساليب Model إلى إعادة تعريف إذن؟

felixfbecker _للبداية_مثال:

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 حسنًا ، لذلك يبدو أن التعليقات التوضيحية this: { new (): T } تجعل الاستدلال على الكتابة يعمل بالفعل. مما يجعلني أتساءل لماذا ليس هذا هو النوع الافتراضي الذي يستخدمه المترجم.
لا يعمل الحل البديل بالطبع مع المُنشئ ، حيث لا يمكن أن تحتوي هذه المعلمات على معلمة this .

نعم ، لا يعمل مع المنشئين ، ولكن يمكنك التغلب على المشكلة بتغيير صغير لواجهة برمجة التطبيقات ، عن طريق إضافة طريقة create ثابتة.

أفهم ذلك ، ولكن هذه مكتبة JavaScript ، لذلك نحن نتحدث عن كتابة API الموجودة في التصريح.

إذا كان this يعني { new (): T } داخل طريقة ثابتة ، فلن يكون this هو النوع الصحيح لـ this داخل الطريقة الثابتة. على سبيل المثال ، لن يُسمح بـ new this() . من أجل الاتساق ، يجب أن يكون this نوع دالة المُنشئ ، وليس نوع مثيل الفئة.

أواجه مشكلة في كتابة مكتبة موجودة. إذا لم يكن لديك سيطرة على تلك المكتبة ، فلا يزال بإمكانك إنشاء فئة أساسية وسيطة ( BaseModel extends Model ) بوظيفة create مكتوبة بشكل صحيح واشتقاق جميع نماذجك من BaseModel .

إذا كنت تريد الوصول إلى الخصائص الثابتة للفئة الأساسية ، فيمكنك استخدامها

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

لكن ربما لديك نقطة صحيحة حول المنشئ. لا أرى سببًا قويًا لضرورة قيام المترجم برفض ما يلي:

constructor(values: Partial<this>) {}

أتفق مع xealot على أن هناك ألمًا هنا. سيكون رائعًا جدًا إذا تمكنا من الكتابة

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

بدلا من

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

لكننا نحتاج إلى عامل جديد في نظام الكتابة ( instanceof ؟). ما يلي غير صالح:

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

كسر TS 2.4 هذه الحلول بالنسبة لي:
https://travis-ci.org/types/sequelize/builds/247636686

sandersnfelixfbecker أعتقد أن هذا خطأ صالح. ومع ذلك ، لا يمكنني إعادة إنتاجه بأسلوب بسيط. معامل رد الاتصال مخالف.

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

أي فرصة هذا سيتم إصلاحه في مرحلة ما؟

واجهت هذا اليوم أيضًا. أردت أساسًا بناء فئة أساسية مفردة ، مثل هذا:

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

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

سيكون الاستخدام شيئًا مثل:

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

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

لقد جربت حوالي 10 أشكال مختلفة من هذا ، مع المتغير StaticThis المذكور أعلاه ، والعديد من الأنواع الأخرى. في النهاية ، هناك إصدار واحد فقط يمكن تجميعه ، ولكن بعد ذلك تم اشتقاق نتيجة getInstance() على أنها Object فقط.

أشعر أن هذا أصعب بكثير مما يجب أن يكون عليه العمل مع مثل هذه.

الأعمال التالية:

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

الجزء this as any as typeof Singleton قبيح لكنه يشير إلى أننا نخدع نظام الكتابة حيث يجب تخزين _instance على مُنشئ الفئة المشتقة.

عادةً ، سيتم دفن الفئة الأساسية في إطار العمل الخاص بك حتى لا تضر كثيرًا إذا كان تنفيذها قبيحًا بعض الشيء. ما يهم هو الكود النظيف في الفئات المشتقة.

كنت أتمنى أن أكون قادرًا في 2.8 ليلة على القيام بشيء مثل هذا:

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

للأسف لا يوجد مثل هذا الحظ :(

تعمل أمثلة التعليمات البرمجية bjouhier مثل السحر. لكن كسر IntelliSense الإكمال التلقائي.

تم إصدار React 16.3.0 ويبدو أنه من المستحيل كتابة طريقة getDerivedStateFromProps الجديدة بشكل صحيح ، نظرًا لأن الطريقة الثابتة لا يمكن أن تشير إلى معلمات نوع الفئة:

(مثال مبسط)

class Component<P, S> {

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

    props: P
    state: S
}

هل هناك أي عمل في الأرجاء؟ (لا أحسب "كتابة P و S على أنها PP و SS وآمل أن يجعل المطورون هاتين العائلتين من النوعين متطابقتين تمامًا" على أنها واحدة: p)

العلاقات العامة: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24577

cncolder لقد شاهدت كسر التحسس أيضًا. أعتقد أنه بدأ مع النسخة المطبوعة 2.7.

شيء واحد لا أراه هنا ، يمتد إلى مثال Singleton - سيكون نمطًا شائعًا لجعل فئة Singleton / Multiton بها مُنشئ محمي - ولا يمكن تمديد هذا إذا كان النوع { new(): T } مثل هذا يفرض مُنشئًا عامًا - يجب أن يحل المصطلح this أو أي حل آخر متوفر هنا ( instanceof this ، typeof this إلخ) هذه المشكلة. هذا ممكن بالتأكيد بإلقاء خطأ في المُنشئ إذا لم يطلب المفرد إنشائه وما إلى ذلك ، لكن هذا يلغي الغرض من كتابة الأخطاء - للحصول على حل وقت التشغيل ...

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

لا أعرف سبب احتياجك إلى مفرد في TS getInstance() ، يمكنك فقط تحديد الكائن الحرفي. أو إذا كنت تريد تمامًا استخدام الفئات (للأعضاء الخاصين) ، فقم بتصدير نسخة الفئة فقط وليس الفئة ، أو استخدم تعبير الفئة ( export new class ... ).

هذا أكثر فائدة لـ Multiton - وأعتقد أنني وجدت نمطًا يعمل ، ... على الرغم من أنه لا يزال شعورًا صعبًا للغاية بدون هذا النوع .. بدلاً من ذلك ، هذا ضروري

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

لدي أيضًا بعض التعليمات البرمجية لتخزين المفتاح بحيث يمكن الوصول إليه في فصل الأطفال عند الإنشاء ، لكنني جردته من أجل البساطة ...

مرحبا. أواجه مشكلة تحتاج إلى "هذا" متعدد الأشكال للمنشئ. على غرار هذا التعليق.

أريد إنشاء فصل دراسي. يحتوي الصنف نفسه على العديد من الخصائص ، لذا لن يبدو المُنشئ جيدًا إذا تمت تهيئة جميع خصائص تلك الفئة عبر معلمات المُنشئ.

أفضل المُنشئ الذي يقبل كائنًا لتهيئة الخصائص. فمثلا:

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

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

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

بعد ذلك ، أريد تمديد الفصل. فمثلا:

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

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

سيكون من الجيد جدًا استخدام نوع this على المُنشئ أيضًا.

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

لذلك ، يمكن لـ IntelliSense استنتاج خصائص الفئة الفرعية بدون دالة مرجعية إضافية: على سبيل المثال ، إعادة تعريف المُنشئ. سيبدو طبيعيًا أكثر من استخدام طريقة ثابتة لتهيئة الفصل.

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

شكرا جزيلا لتفهمك.

أنا سعيد لأنني وجدت هذا الموضوع. أعتقد بصدق أن هذه مشكلة خطيرة للغاية وقد كنت محبطًا للغاية لرؤيتها تنتقل إلى 3.0.

بالنظر إلى جميع الحلول والنظر في الحلول المختلفة التي استخدمتها على مدار العامين الماضيين ، توصلت إلى استنتاج مفاده أن أي حل سريع نحاوله سيفشل ببساطة إذا وعندما يتم التوصل إلى حل. وحتى ذلك الحين ، فإن كل إكراه نقوم به يتطلب ببساطة الكثير من العمل اليدوي للحفاظ عليه في جميع مراحل المشروع. وبمجرد نشره ، لا يمكنك مطالبة المستهلكين بفهم عقليتك الخاصة ولماذا اخترت إصلاح شيء ما وهو TypeScript إذا كانوا يستهلكون الحزمة الخاصة بك في الفانيليا ولكن مثل معظمنا ، يعمل مع VSCode.

في الوقت نفسه ، فإن كتابة التعليمات البرمجية لجعل نظام الكتابة سعيدًا عندما لا تكون اللغة الفعلية هي المشكلة ، تتعارض مع نفس سبب استخدام الميزات المكتوبة للبدء بها.

توصيتي للجميع بقبول أن الخطوط المتعرجة خاطئة واستخدام //@ts-ignore because my code works as expected عندما يكون ذلك ممكنًا.

إذا كان لينت الخاص بك محصنًا ضد الذكاء البشري المعقول ، فقد حان الوقت للعثور على شخص يعرف مكانه.

تحديث :
كنت أرغب في إضافة محاولتي الخاصة لزيادة الاستدلال الثابت بشكل آمن . هذا النهج محكم تمامًا لأنه يُقصد به أن يكون بعيدًا عن الطريق. إنه صريح بشكل شامل ، مما يجبرك على التحقق من الزيادات الخاصة بك وليس مجرد افتراض أن الميراث سوف يؤدي العمل نيابة عنك.

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

من الواضح أنني لا أستطيع أن أضمن بقاء هذا النمط ، أو أنه سيرضي كل سيناريو في هذا الموضوع ، لكنه في الوقت الحالي ، يعمل كما هو متوقع.

تنبع هذه الاختيارات من الإدراك المقلق أنه حتى هذه النقطة تتضمن ملفات lib الخاصة بـ TypeScript اثنين فقط من تعريفات الفئة!

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

ناقش لفترة اليوم. النقاط الرئيسية:

  • هل يشير this إلى جانب المثيل أم إلى الجانب الثابت؟

    • بالتأكيد الجانب الساكن. يمكن الإشارة إلى جانب المثيل عبر InstanceTypeOf لكن العكس ليس صحيحًا. يحافظ هذا أيضًا على التناظر بأن this في التوقيع له نفس نوع this في النص.

  • مشاكل السلامة

    • لا توجد ضمانات على أن قائمة معلمات مُنشئ الفئة المشتقة لها أي علاقة بقائمة معلمات مُنشئ الفئة الأساسية الخاصة بها

    • تكسر الفئات قابلية استبدال الجانب الثابت في هذا الصدد في كثير من الأحيان

    • يتم فرض استبدال جانب البناء وهذا عادة ما يهتم به الناس على أي حال

    • نحن نسمح بالفعل بالاستدعاءات غير السليمة لـ new this() static

    • من المحتمل ألا يهتم أحد بجانب توقيع البناء الثابت this من الخارج؟

الحلول الحالية:

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

والجدير بالذكر أن هذا لا يعمل إلا إذا كان توقيع بناء Bar قابلاً للاستبدال بشكل صحيح لـ Foo 's

RyanCavanaugh لقد كنت أحاول التوصل إلى تفسير حيث تأتي قيمة this في مثالك static boo<T extends typeof Foo>(this: T): InstanceType<T> ، لكني لم أحصل عليها. للتأكد من أنني أعيد قراءة كل شيء عن الأدوية الجنيسة ، ولكن لا يزال - كلا. إذا استبدلت this بـ clazz على سبيل المثال ، فلن يعمل بعد الآن وسيشتكي المترجم من "Expected 1 arguments، but got 0." لذا فإن بعض السحر يحدث هناك. هل يمكن ان توضح؟ أو أشرني إلى الاتجاه الصحيح في التوثيق؟

تعديل:
أيضًا ، لماذا هو extends typeof Foo وليس مجرد extends Foo ؟

creynders this معلمة زائفة خاصة يمكن كتابتها لسياق الوظيفة. المستندات .

استخدام InstanceType<T> لا يعمل عندما تكون هناك أدوية متضمنة.

تبدو حالة الاستخدام الخاصة بي كما يلي:

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

رابط ملعب TypeScript

mastermatt أوه ، واو ، لم تلتقط ذلك تمامًا عند قراءة المستندات ... شكرًا

بالنظر إلى مثالRyanCavanaugh ، تمكنت من تشغيل أساليبنا الثابتة ، لكن لدي أيضًا طرق مثيلة تشير إلى الأساليب الثابتة ، لكن لا يمكنني معرفة كيفية الحصول على الكتابة الصحيحة لذلك.

مثال:

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

إذا تمكنت من إيجاد حل بديل لهذه المشكلة الأخيرة ، فسيتم تعيينها حتى يأتي شيء رسمي.

وتجدر الإشارة أيضًا إلى هذا المثال ، إذا حاولت الفئة الفرعية تجاوز أي طريقة ثابتة ثم اتصلت super ، فسيكون الأمر مستاءً أيضًا ... بخلاف هاتين المسألتين ، يبدو أن الحل البديل يعمل بشكل جيد. على الرغم من أن هاتين المسألتين تحظران إلى حد ما الكود الخاص بنا.

IIRC هذا لأن هذا لا يمكن التعبير عنه في الكتابة المطبوعة دون ثني نظام الكتابة قليلاً. كما في ؛ this.constructor ليس مضمونًا على الإطلاق أن يكون كما تعتقد أو شيء من هذا القبيل.

في هذه الحالة بالتحديد ، سأحصل على طريقة المثيل boo() بإرجاع this وتخدع قليلاً في الداخل ، مما يجبر المترجم على قبول أنني أعرف ما أفعله.

المنطق العام لدي هو أنني أريد أن تكون واجهة برمجة التطبيقات بسيطة قدر الإمكان لبقية الكود ، حتى لو كان ذلك يعني في بعض الأحيان الغش قليلاً.

إذا كان لدى شخص ما شيء أكثر قوة ، فأنا أحب ذلك

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

يمكنك أيضًا الانتقال إلى مسار الواجهة والقيام بشيء من هذا القبيل ؛

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

هذا غش أيضًا ، لكن ربما يكون أكثر تحكمًا؟ لا استطيع ان اقول حقا.

إذن ، يتم التعامل مع الوسيطة الأولى ، this في طريقة "boo" الثابتة بشكل خاص ، وليس كوسيطة عادية؟ أي روابط للمستندات تصف هذا؟

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

أواجه نفس المشكلة (على الأقل مماثلة).
أنا أستخدم Vanilla Javascript مع JSDoc (وليس TypeScript) ، لذلك لا يمكنني استخدام / تنفيذ الحلول التي تدور حول الأدوية العامة المقترحة في هذا الموضوع ، ولكن يبدو أن الجذر هو نفسه.
لقد فتحت هذا العدد: # 28880

هذه القضية حرفيا 3 سنوات بالفعل.
هل وجد أي شخص حلاً مناسبًا يمكن أن يعمل لي؟

ثلاث سنوات

يعد عمل RyanCavanaugh رائعًا للطرق الثابتة ، ولكن ماذا عن الموصل الثابت؟

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

ثلاث سنوات

حسنًا ، أنت نصف لائق في طرح التواريخ. لكن هل يمكنك تقديم حل؟ ؛)

RyanCavanaugh هذه مشكلة عندما نستخدم this في المولدات على النحو التالي:

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

عندما نصنع مولدات ، لا يمكننا استخدام وظائف الأسهم لاستخدام this . لذا علينا الآن أن نعلن صراحة عن كتابة this في الفئات الفرعية. الحالة الفعلية هي كما يلي:

      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

لا يتعين علينا الإعلان عن النوع this في الفئات الفرعية إذا كان بإمكاننا القيام بذلك في الفئات الأساسية. ومع ذلك ، لا يتم تهيئة قيمة this عندما يتم استدعاء المولد بشكل متزامن في الفئة الممتازة. لقد تجنبت هذه المشكلة في الكود ولكن هذا اختراق سيء. لذلك قد يكون هذا النمط غير متطابق في الأصل مع لغات البرمجة القائمة على الفصل.

ليس هو الأنظف ، ولكنه قد يكون مفيدًا للوصول إلى فئة الطفل من طريقة ثابتة.

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

class C extends Base {
}

C.foo();

على الرغم من أنني لست متأكدًا مما إذا كانت المشكلة التي أواجهها حاليًا ترجع إلى هذا ، ولكن بعد لمحة موجزة عن هذه المشكلة ، يبدو أن هذا هو الحال على الأرجح. يرجى التصحيح إذا لم يكن هذا هو الحال.

لذلك لدي الفئتان التاليتان المرتبطتان عن طريق الميراث.

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

بعد ذلك ، أقوم بإضافة طريقة mapObject في فئة SubTarget على النحو التالي.

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

على الرغم من أنني توقعت أن يعمل هذا ، إلا أنني تلقيت الخطأ التالي.

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

هل تم إنشاء هذا الخطأ بسبب هذه المشكلة؟ خلاف ذلك ، سيكون التفسير رائعًا حقًا.

رابط إلى الملعب

أتيت هنا أثناء البحث عن حل لإضافة تعليق توضيحي للطرق الثابتة التي تُرجع مثيلات جديدة للفئة الفعلية التي تم استدعاؤها.

أعتقد أن الحل الذي اقترحه SMotaal (بالاشتراك مع new this() ) هو الحل الأنظف والأكثر منطقية بالنسبة لي. إنه يسمح بالتعبير عن النية المرغوبة ، ولا يجبرني على تحديد النوع في كل استدعاء للطريقة العامة ولا يضيف أي عبء في الكود النهائي.

ولكن بجدية ، كيف لم يعد هذا جزءًا من كتابات التنقيح الأساسية حتى الآن؟ إنه سيناريو شائع إلى حد ما.

danielvy - أعتقد أن الانفصال بين OOP والنماذج الأولية هو الفجوة العقلية الأكبر التي تفشل في إسعاد الجميع. أعتقد أن الافتراضات الأساسية حول الفصول تراجعت قبل أن تتطور ميزات الفصل في المواصفات ، والانفصال عن ذلك يمثل مأزقًا للعديد من ميزات TypeScript العاملة ، لذا فإن الجميع "يعطي الأولوية" وهذا أمر جيد ولكن ليس من أجل " أنواع "في رأيي. إن عدم وجود حل أفضل من المنافسة هو مشكلة فقط إذا كانت هناك منافسة ، وهذا هو السبب في أن كل البيض في سلة واحدة يكون دائمًا سيئًا للجميع - أنا عملي وصادق هنا.

هذا لا يعمل:

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.

لكن ألا ينبغي السماح بذلك لأن الامتداد يمتد للقاعدة؟

لاحظ أن هذا يعمل:

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

ولكن بعد ذلك لا يمكنك استخدام هذا العضو بداخله.

لذلك أعتقد من مشاركة ntucker أن الحل البديل يعمل فقط بمستوى واحد عميق ما لم تتطابق الفئة الممتدة مع الفئة الأساسية تمامًا؟

مرحبا! ماذا عن المنشئات المحمية ؟

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

  protected constructor() {}
}

class B extends A {} 

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

إذا كان المُنشئ عامًا - فلا بأس بذلك ، ولكن إذا لم يكن كذلك ، فإنه يتسبب في حدوث خطأ:

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.

ملعب - http://tiny.cc/r74c9y

هل هناك أي أمل في هذا؟ عثرت للتو على هذه الحاجة مرة أخرى: F

tucker Duuuuudee ، لقد فعلتها !!! كان الإدراك الأساسي بالنسبة لي هو جعل طريقة الإنشاء الثابتة عامة ، والإعلان عن معلمة this ، ثم القيام بعملية التمثيل InstanceType<U> في النهاية.

ملعب

من فوق ^

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 😄 

هذا يرضي تمامًا حالة الاستخدام الخاصة بي ، ويبدو أنه اعتبارًا من TS 3.5.1 ، فإن هذا يرضي غالبية الأشخاص في هذا الموضوع أيضًا (انظر ساحة اللعب لاستكشاف الدعامة الثابتة theAnswer بالإضافة إلى المستوى الثالث من الامتداد)

Hot take: أعتقد أن هذا يعمل الآن ويمكن إغلاقه؟

jonjaques Btw أنت لا تستخدم أبدًا T. أيضًا ، هذا لا يحل المشكلة التي أشرت إليها وهي الطرق المهيمنة.

أشعر أن الحل المذكور قد تم اقتراحه بالفعل منذ ما يقرب من 4 سنوات ... ومع ذلك فهو ليس بالشيء الذي يحل المشكلة.

فتحت سيرًا مكدسًا لمعرفة ما إذا كانت هناك حلول أخرى قد يعرفها شخص ما وكان لدى شخص ما ملخصًا جيدًا للمشكلة التي أواجهها:

"العوامل العامة المستخدمة كمعلمات للطريقة ، بما في ذلك هذه المعلمات ، تبدو متناقضة ، لكنك تريد دائمًا معاملة هذه المعلمات على أنها متغير مشترك (أو ربما حتى متغير ثنائي)."

لذلك يبدو أن هذا شيء تم كسره بالفعل في TypeScript نفسها ويجب إصلاحه (يجب أن يكون "هذا" متغيرًا أو ثنائيًا).

تحرير: هذا التعليق لديه طريقة أفضل.

ليس لدي الكثير لأضيفه ، لكن هذا كان جيدًا بالنسبة لي من 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()

آمل أن يكون هذا مفيدًا لأي شخص لا يريد تصفح هذه المشكلة الطويلة.

حالة استخدام أخرى هي وظائف عضو حارس من النوع المخصص على غير الفئات:

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

يكاد الحل يقودني إلى المكان الذي أحتاج أن أكون فيه ، لكن لدي مفتاح ربط آخر لأرمي التروس. خذ المثال المفتعل التالي:

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

ها هو الخطأ:

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)

هذا منطقي بالنسبة لي ، نظرًا لأن طريقة create في فئة Automobile لا تشير إلى ITruckOptions عبر O ، لكني أتساءل عما إذا كان هناك الحل لهذه المشكلة؟

عادةً ما تقوم خيارات الفئة الموسعة الخاصة بي للمُنشئ بتوسيع واجهة خيارات الفئة الأساسية ، لذلك أعلم أنها ستشمل دائمًا معلمات الفئة الأساسية ، ولكن ليس لديها طريقة موثوقة لضمان تضمينها معلمات للتوسيع صف دراسي.

لقد اضطررت أيضًا إلى اللجوء إلى طرق تجاوز فقط في توسيع الفصول لإبلاغهم بأنواع المدخلات والعودة المتوقعة للفئة الوراثية التي تشعر برائحة كريهة قليلاً.

@ جاك باري هذا يعمل:

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

ومع ذلك ، لا يزال هذا غير مثالي نظرًا لأن المنشئين متاحون للجمهور ، لذا من الممكن فقط القيام بـ new Automobile({ make: "Audi" }) .

elderapo أعتقد أن هذا يمنحني ما أحتاجه - المثال مفتعل بالطبع لكني أرى حيث قلة فهمي للجنرالين قد عضني قليلاً. شكرا للتوضيح!

آمل أن يساعد هذا الآخرين ، اسمحوا لي أن أعرف إذا كان هناك أي مجال للتحسين. إنها ليست مثالية ، حيث من المحتمل أن تصبح وظائف القراءة غير متزامنة مع الكتابة ولكنها أقرب ما يمكنني الوصول إليه من النمط الذي نحتاجه.

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

يمكنك بسهولة تمديد هذا النمط إلى إجراءات CRUD المتبقية.

في مثال @ Jack-Barry ، أحاول الحصول على الدالة read لإرجاع مثيل للفئة. كنت أتوقع أن يعمل ما يلي:

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

ولكن بدلاً من ذلك تحصل على الخطأ Property 'create' does not exist on type 'ThisClass<A, T>'. هل سيكون لدى أي شخص حل بديل حول كيفية معالجة هذا؟

everhardt نظرًا لأن الطريقة create هي static ، يجب استدعاؤها على class من this ، وليس المثيل. ومع ذلك ، ليس لدي حقًا حل جيد بعيدًا عن رأسي حول كيفية الوصول إلى وظيفة class ديناميكيًا والتي لا تزال توفر فحص النوع المطلوب.

أقرب ما يمكنني الحصول عليه ليس أنيقًا بشكل خاص ، ولا يمكنني استخدام type الحالي من BaseClass.create ، لذلك يمكن أن يصبحوا غير متزامنين بسهولة:

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

لم _ لم أختبر هذا.

@ Jack-Barry ثم تحصل على this.constructor.create is not a function .

هذا أمر منطقي: إن Typescript تفسر this كمثال ، ولكن بالنسبة لمحلل جافا سكريبت النهائي ، فإن this هي الفئة حيث أن الوظيفة ثابتة.

ربما ليس الامتداد الأكثر إلهامًا لمثال جاك باري ، ولكن في رابط الملعب هذا ، يمكنك أن ترى كيف حللت ذلك.

جوهر:

  • يجب أن يصف النوع ThisClass جميع الخصائص والطرق الثابتة التي تريد أساليب BaseClass (سواء كانت ثابتة أو على سبيل المثال) استخدامها مع "this" متعدد الأشكال.
  • عند استخدام خاصية أو طريقة ثابتة في طريقة مثيل ، استخدم (this.constructor as ThisClass<A, this>)

إنه عمل مزدوج ، حيث يتعين عليك تحديد أنواع الأساليب والخصائص الثابتة في كل من الفئة ونوع ThisClass ، لكنه يعمل بالنسبة لي.

تحرير: إصلاح رابط الملعب

قام PHP بإصلاح هذه المشكلة منذ وقت طويل. الذات. ثابتة. هذه.

مرحبًا جميعًا ، إنه عام 2020 ، وجود هذه المشكلة. 😛

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

أتوقع أن يكون bugs مثيلًا لـ Bunny .

ما هو الحل البديل الحالي؟ لقد جربت كل شيء في هذه المشكلة ، ولا يبدو أنه تم إصلاح أي شيء.

تحديث : لقد نجح هذا الأمر ، وغاب عن تعريف الوسيطة 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

تكمن المشكلة في ذلك في أنه لا يمكن الحصول على معلمات الكتابة والمحددات.

كما أنها مطولة للغاية.

أنا أتعامل مع مشكلة مماثلة. لتنفيذ "فئة داخلية متعددة الأشكال" ، فإن أفضل ما يمكن أن أجده هو ما يلي:

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

يبدو أنه يعمل بشكل جيد ، ولكن كتابة createInnerClass() مطولة للغاية في رأيي ، وسيكون لدي الكثير من هؤلاء في قاعدة الشيفرة الخاصة بي. هل لدى أي شخص أي أفكار حول كيفية تبسيط هذا الرمز؟

تكمن المشكلة في هذا النهج في أنه لا يعمل مع أدوات القياس الثابتة.

ماذا عن تطبيق self. للمواقع الثابتة؟ لقد واجهت هذه المشكلة منذ 4 سنوات ، وسأعود إلى نفس الموضوع بنفس المشكلة.

sudomaxime هذا خارج نطاق هذه المشكلة ويعمل بشكل مخالف لأهداف تصميم TypeScript طالما أن ECMAScript لا يدعم في الأصل self. كاسم مستعار لـ this.constructor. في الفصول الدراسية ، وهو أمر غير مرجح يحدث بالنظر إلى أن self هو اسم متغير شائع.

يبدو أن حل @ abdullah-kasim يعمل كما هو موصوف ، لكنني غير قادر على جعله يعمل مع فئات عامة:

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

سأقدر أي أفكار حول ما إذا كان هذا ممكنًا لأنني تلاعبت به قليلاً دون حظ.

stevehanson هل هذا العمل بالنسبة لك؟

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

اختبار هذا في ملعب مطبوع.

مستوحى من هذا الرابط:
https://selleo.com/til/posts/gll9bsvjcj-generic-with-class-as-argument-and-returning-instance

ولست متأكدًا من الكيفية التي استطاعت بها الإشارة إلى أن T هي الفصل نفسه . تحرير: آه ، تذكرت ، تمكنت من الإشارة إلى T لأن المعلمة this الصامتة ستمرر الفصل نفسه دائمًا.

@ عبدالله قاسم هذا نجح! نجاح باهر شكرا لك! بالنسبة لحالتي المحددة ، فإن المنشئ الخاص بي يأخذ حجة ، لذلك كان هذا ما بدا لي ، في حال كان يساعد أي شخص آخر:

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

بعض الحلول هنا لا تعمل مع رموز ثابتة لأنني لا أستطيع تمرير أي حجج في:

بالنظر إلى المثال أدناه ، كيف يمكنني نقل الطالب الثابت default إلى فصل دراسي أصلي؟

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

const x = Letters.default.alpha;

قد يكون هذا أمرًا يستحق التفكير إذا نظرنا في تحسينات بناء الجملة.

ذات صلة محتملة: أجد صعوبة في إضافة طبقة أخرى من التجريد لمثال @ abdullah-kasim. بشكل أساسي ، أود أن أكون قادرًا على تحويل Animal إلى واجهة ، والسماح لـ Bunny بتعريف ثابت create() الخاص به.

إليك مثال عملي (سامحني لتخلي عن تشبيه الأرنب!). أريد أن أكون قادرًا على تحديد بعض أنواع المستندات المختلفة ، ويجب أن يكون كل منها قادرًا على تحديد طريقة مصنع ثابتة تحول سلسلة إلى مثيل مستند. أريد أن أفرض أن نوع المستند يجب أن يحدد محللًا لنفسه فقط ، وليس لأنواع المستندات الأخرى.

(الأنواع الموجودة في المثال أدناه ليست صحيحة تمامًا - آمل أن يكون واضحًا بدرجة كافية ما أحاول تحقيقه)

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

(أعتقد) أود أن أتمكن من كتابة شيء مثل هذا:

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

هل لديك أي فكرة عما إذا كان الحل ممكنًا هنا أيضًا؟

تحرير: الحل الممكن

حالة استخدام أخرى في StackOverflow
لتصميم فئة جدول بحث عام.

// 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 علق في 29 مايو 2016
😰 لا أصدق أن هذا لم يتم إصلاحه / إضافته ...

ها ها
2020 هنا!

هذا سخيف ومجنون. إنه عام 2020. بعد 4 سنوات ، لماذا لم يتم إصلاح ذلك؟

لماذا لا تنفذ شيئا مثل

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
هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات

القضايا ذات الصلة

seanzer picture seanzer  ·  3تعليقات

weswigham picture weswigham  ·  3تعليقات

DanielRosenwasser picture DanielRosenwasser  ·  3تعليقات

MartynasZilinskas picture MartynasZilinskas  ·  3تعليقات

jbondc picture jbondc  ·  3تعليقات