Typescript: Полиморфное "это" для статических членов

Созданный на 1 дек. 2015  ·  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

Самый полезный комментарий

Есть ли причина, по которой этот вопрос был закрыт?

Тот факт, что polymorphic this не работает на статике, делает эту функцию DOA, на мой взгляд. На сегодняшний день я никогда не нуждался в полиморфном this для элементов экземпляра, но мне нужно было каждые несколько недель в статике, так как система обработки статики была доработана еще в первые дни. Я был вне себя от радости, когда было объявлено об этой функции, а затем разочаровался, когда понял, что она работает только на членах инстанса.

Вариант использования очень простой и чрезвычайно распространенный. Рассмотрим простой фабричный метод:

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.

Я тоже столкнулся с этой проблемой сегодня!

Решение Fixup состоит в том, чтобы передать дочерний тип как общий, расширяющий базовый класс, и это решение, которое я применяю в настоящее время:

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

Есть ли причина, по которой этот вопрос был закрыт?

Тот факт, что polymorphic this не работает на статике, делает эту функцию DOA, на мой взгляд. На сегодняшний день я никогда не нуждался в полиморфном this для элементов экземпляра, но мне нужно было каждые несколько недель в статике, так как система обработки статики была доработана еще в первые дни. Я был вне себя от радости, когда было объявлено об этой функции, а затем разочаровался, когда понял, что она работает только на членах инстанса.

Вариант использования очень простой и чрезвычайно распространенный. Рассмотрим простой фабричный метод:

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 Я тоже был разочарован этой проблемой, но ниже приведен самый надежный обходной путь, который я нашел. Каждому подклассу Animal нужно будет вызвать super.create() и просто привести результат к своему типу. Не имеет большого значения, и это один вкладыш, который можно легко удалить с помощью этого.

Компилятор, intellisense и самое главное зайчик всем довольны.

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 Да ... отсюда и «прибегание к уродливому приведению или замусориванию статических методов create () в каждом наследнике». Однако это довольно сложно, когда вы разработчик библиотеки и не можете заставить конечных пользователей реализовать кучу типизированных статических методов в классах, которые они унаследовали от вас.

закон. Полностью пропустил все под вашим кодом: D

Мех того стоило, надо нарисовать кролика.

:+1: кролик

:кролик: :сердце:

+1, определенно хотелось бы увидеть это

Были ли обновления обсуждений на эту тему?

Он остается в нашем огромном списке предложений.

Javascript уже работает правильно в таком шаблоне. Если бы TS также мог следовать, это избавило бы нас от большого количества шаблонного/дополнительного кода. «Шаблон модели» довольно стандартный, я ожидаю, что TS будет работать так же, как JS.

Мне также очень понравилась бы эта функция по тем же причинам «Модели CRUD», что и всем остальным. Мне это нужно для статических методов больше, чем для методов экземпляра.

Это обеспечило бы изящное решение проблемы, описанной в #8164.

Хорошо, что есть «решения» с переопределениями и дженериками, но на самом деле они здесь ничего не решают — вся цель наличия этой функции состоит в том, чтобы избежать таких переопределений/приведения и создать согласованность с тем, как возвращается this тип обрабатывается в методах экземпляра.

Я работаю над типизацией для Sequelize 4.0, и в ней используется подход, при котором вы создаете подкласс класса Model . Этот класс 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;
    }
}

Ожидается: это будет доступно для параметров конструктора.

Проблема, которую предполагается решить:

  • иметь возможность основывать свой свободный API либо вокруг себя, либо вокруг другого свободного API, повторно используя тот же код:
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 Это не было бы непоследовательным. Когда вы указываете класс в качестве типа возвращаемого значения для такой функции, как User, тип возвращаемого значения будет экземпляром пользователя. Точно так же, когда вы определяете возвращаемый тип this в статическом методе, возвращаемый тип будет экземпляром this (экземпляром класса). Затем метод, который возвращает сам класс, может быть смоделирован с помощью 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 , возвращаемый из статической функции, представляет экземпляр, поскольку он определен внутри типа, но это я. Другие могут думать иначе, и мы не можем сказать им, что они ошибаются.

Проблема в том, что должна быть возможность ввести как статический метод, возвращающий экземпляр класса, так и статический метод, возвращающий сам класс (тип this). Ваша логика имеет смысл с точки зрения JS, но здесь мы говорим о возвращаемых _types_, и использование класса в качестве типа возвращаемого значения (здесь this) в TypeScript и любом другом языке всегда означает экземпляр класса. Чтобы вернуть фактический класс, у нас есть оператор typeof.

@felixfbecker Это поднимает еще одну проблему!

Если тип this является типом экземпляра, он отличается от того, на что ссылается ключевое слово this в теле статического метода. return this возвращает тип возврата функции typeof this , что совершенно странно!

Нет, это не так. Когда вы определяете такой метод, как

getUser(): User {
  ...
}

вы ожидаете вернуть User _instance_, а не класс User (для этого и нужен 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 в статическом методе, я думаю, что это весь корень проблемы.

@felixfbecker

Учти это:

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

Эта аннотация типа интуитивно понятна, но неверна, если тип this в статическом методе является экземпляром класса. Ключевое слово this имеет тип typeof this в статическом методе!

Я действительно считаю, что this _должно_ относиться к типу экземпляра, а не к типу класса, потому что мы можем получить ссылку на тип класса из типа экземпляра ( typeof X ), но не наоборот ( instancetypeof X ?)

@xealot хорошо, почему бы не использовать ключевое слово static вместо this ? Но this в статическом контексте JS по-прежнему указывает на конструктор.

@izatop да, сгенерированный javascript работает (правильно или неправильно). Однако это не касается Javascript. Жалоба не в том, что язык 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 поставляется с typescript 2.0.0, который позволяет определять параметр this для функций (что позволяет использовать this в их теле без проблем, а также для предотвращения неправильного использования функций)

Оказывается, мы можем использовать их как в методах, так и в статических методах, и что они позволяют делать действительно забавные вещи, например, использовать их для «помощи» устройству вывода типов, заставляя его сравнивать this , которые мы предоставляем (ИМодель) к используемому (собака или кошка). Затем он «угадывает» правильный тип для 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.

Обратите внимание, что это довольно важно, поскольку это часто используемый шаблон в ORM, и большая часть кода ведет себя совсем не так, как думают компилятор и система типов.

Я бы привел несколько контраргументов против контраргумента.

Как указал @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. make 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 на самом деле не является статическим использованием, это отложенное использование, которое будет вычислено позже. Однако статические методы (включая конструктор) работают иначе.

Я думаю, что во время выполнения все это работает так, как ожидалось. Однако система типов не способна моделировать этот конкретный шаблон. Неспособность машинописного текста моделировать шаблон javascript является причиной того, почему этот вопрос остается открытым.

Извините, если это запутанный комментарий, написанный на скорую руку.

@xealot Я понимаю вашу точку зрения, но другие проблемы, которые специально предлагали полиморфные this для статических методов, были закрыты как дубликат этой. Я предполагаю, что исправление в TS позволит использовать оба варианта использования.

Каков ваш точный вариант использования? Может быть, «этого» решения достаточно.

Я успешно использую его в пользовательской библиотеке ORM.
Le jeu. 20 окт. 2016 в 19:15, Том Мариус, уведомления@github.com
текст:

Обратите внимание, что это довольно важно, так как это обычно используется
шаблон в ORM и большая часть кода ведут себя совсем иначе, чем
компилятор и система типов считают, что должны.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на 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();

И неизбежно, что static иногда относится к типам экземпляров.

У меня была другая идея относительно обсуждения того, должен ли тип 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 просили об этой функции.

@pleerock @xealot Решения были предложены выше:

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 с этим вопросом и решениями для справки?

@bjouhier @ceymard Я объяснил, почему все обходные пути в этой теме не работают для 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 не может легко моделировать Javascript.

Насколько я понимаю, Typescript должен быть расширением Javascript.

@felixfbecker

Это означало бы, что вам нужно переобъявить практически каждый метод в каждом подклассе модели.

Почему? Это совсем не мой опыт. Можете ли вы проиллюстрировать проблему примером кода?

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

Да, это не работает для конструкторов, но вы можете обойти проблему небольшим изменением API, добавив статический метод 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

@sandersn @felixfbecker Я думаю, что это действительная ошибка. Однако я не могу воспроизвести его в минимальной форме. Параметр обратного вызова является контравариантным.

// 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 nightlies можно будет сделать что-то вроде этого:

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)

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

@cncolder Я также был свидетелем взлома intellisense. Я считаю, что это началось с машинописного текста 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

У меня также есть некоторый код для хранения ключа, чтобы он был доступен в дочернем классе при создании, но я убрал его для простоты...

Привет. Я столкнулся с проблемой, которая требует полиморфного "this" для конструктора. Аналогично этому комментарию.

Я хочу создать класс. Сам класс имеет довольно много свойств, поэтому конструктор не будет хорошо выглядеть, если все свойства этого класса будут инициализированы через параметры конструктора.

Я предпочитаю конструктор, который принимает объект для инициализации свойств. Например:

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, если они просто используют ваш пакет в vanilla, но, как и большинство из нас, работают с 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 включали только два объявления класса!

Безопасный массив

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

@RyanCavanaugh Я пытался придумать объяснение, откуда в вашем примере берется значение this static boo<T extends typeof Foo>(this: T): InstanceType<T> , но я просто не понимаю. Для верности перечитал все по дженерикам, но все равно - нет. Если я заменю, например, this на clazz , это больше не будет работать, и компилятор жалуется: «Ожидается 1 аргумент, но получено 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 и немного обманывал внутри, заставляя компилятор признать, что я знаю, что делаю.

Моя основная причина заключается в том, что я хочу, чтобы API был как можно более простым для остального кода, даже если иногда это означает небольшой обман.

Если у кого-то есть что-то помощнее, буду рад

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

Это тоже обман, но, может быть, немного более контролируемый? Я не могу сказать.

Итак, 1-й аргумент 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();

Хотя я не уверен, что проблема, с которой я сталкиваюсь в настоящее время, связана с этим, но после беглого взгляда на эту проблему кажется, что это, вероятно, так. Пожалуйста, поправьте, если это не так.

Итак, у меня есть следующие 2 класса, связанные через наследование.

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

Затем я добавляю метод SubTarget 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() ), является самым чистым и наиболее понятным для меня. Это позволяет выразить желаемое намерение, не заставляет меня указывать тип при каждом вызове универсального метода и не добавляет никаких накладных расходов в окончательный код.

А если серьезно, как это еще не часть ядра Typescript? Это довольно распространенный сценарий.

@danielvy — я думаю, что разрыв между ООП и прототипами — это большая ментальная пропасть, которая не может сделать всех счастливыми. Я думаю, что основные предположения о классах, сделанные задолго до того, как функции классов развились в спецификации, не совпали, и выход из этого является ловушкой для многих работающих функций 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> {
  }
}

но тогда вы не можете использовать this.member внутри него.

Итак, из сообщения @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

@ntucker 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 , а также 3-го уровня расширения)

Горячий вариант: я думаю, что это работает сейчас и может быть закрыто?

@jonjaques Кстати, вы никогда не используете T. Кроме того, это не решает проблему, которую я обозначил, а именно переопределение методов.

Я чувствую, что упомянутое решение уже было предложено почти 4 года назад… И все же это не то, что решает проблему.

Я открыл stackoverflow , чтобы посмотреть, есть ли другие обходные пути, о которых кто-то может знать, и у кого-то есть хорошее описание проблемы, с которой я сталкиваюсь:

«Обобщения, используемые в качестве параметров метода, включая эти параметры, кажутся контравариантными, но на самом деле вы всегда хотите, чтобы эти параметры рассматривались как ковариантные (или, может быть, даже бивариантные)».

Так что похоже, что это действительно что-то, что сломано в самом 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 Я думаю, что это дает мне то, что мне нужно - пример, конечно, надуманный, но я вижу, что мое непонимание _Generics_ немного меня укусило. Спасибо, что прояснили!

Надеюсь, это поможет другим, дайте мне знать, если есть возможности для улучшения. Это не идеально, так как функции чтения потенциально могут рассинхронизироваться с типизацией, но это самое близкое, что я мог получить к нужному нам шаблону.

// 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 from BaseClass.create , поэтому они могут легко рассинхронизироваться:

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

Я _ не _ проверял это.

@ Джек-Барри, тогда ты получишь this.constructor.create is not a function .

Это имеет смысл: Typescript интерпретирует this как экземпляр, но для возможного синтаксического анализатора javascript this является классом, поскольку функция является статической.

Возможно, это не самое вдохновляющее расширение примера Джека-Барри, но по этой ссылке на игровую площадку вы можете увидеть, как я его решил.

Суть:

  • тип ThisClass должен описывать все статические свойства и методы, которые методы BaseClass (как статические, так и в экземпляре) хотят использовать с полиморфным 'this'.
  • при использовании статического свойства или метода в методе экземпляра используйте (this.constructor as ThisClass<A, this>)

Это двойная работа, так как вам нужно определить типы статических методов и свойств как для класса, так и для типа ThisClass , но для меня это работает.

edit: исправлена ​​ссылка на игровую площадку

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 всегда будет передавать сам класс.

@abdullah-kasim это сработало! Вау, спасибо! В моем конкретном случае мой конструктор принимает аргумент, поэтому для меня это выглядело так, на случай, если это поможет кому-то еще:

  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 рейтинги