Typescript: Предложение: добавьте абстрактные статические методы в классы и статические методы в интерфейсы.

Созданный на 12 мар. 2017  ·  98Комментарии  ·  Источник: microsoft/TypeScript

В качестве продолжения проблемы № 2947, которая разрешает модификатор abstract в объявлениях методов, но запрещает его в объявлениях статических методов, я предлагаю расширить эту функциональность на объявления статических методов, разрешив модификатор abstract static в объявлениях методов. .

Связанная проблема связана с модификатором static в объявлении методов интерфейса, который запрещен.

1. Проблема

1.1. Абстрактные статические методы в абстрактных классах

В некоторых случаях использования абстрактного класса и его реализаций мне могут потребоваться некоторые значения, зависящие от класса (не зависящие от экземпляра), к которым следует обращаться в контексте дочернего класса (не в контексте объекта), без создание объекта. Функция, которая позволяет это сделать, — это модификатор static в объявлении метода.

Например (пример 1):

abstract class AbstractParentClass {
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

FirstChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class FirstChildClass'
SecondChildClass.getSomeClassDependentValue(); // returns 'Some class-dependent value of class SecondChildClass'

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

Например (пример 2):

abstract class AbstractParentClass {
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

abstract class AbstractParentClassFactory {

    public static getClasses(): (typeof AbstractParentClass)[] {
        return [
            FirstChildClass,
            SecondChildClass
        ];
    }
}

var classes = AbstractParentClassFactory.getClasses(); // returns some child classes (not objects) of AbstractParentClass

for (var index in classes) {
    if (classes.hasOwnProperty(index)) {
        classes[index].getSomeClassDependentValue(); // error: Property 'getSomeClassDependentValue' does not exist on type 'typeof AbstractParentClass'.
    }
}

В результате компилятор решает, что произошла ошибка, и выводит сообщение: Свойство «getSomeClassDependentValue» не существует для типа «typeof AbstractParentClass».

1.2. Статические методы в интерфейсах

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

Например (пример 3):

interface Serializable {
    serialize(): string;
    static deserialize(serializedValue: string): Serializable; // error: 'static' modifier cannot appear on a type member.
}

При компиляции этого кода возникает ошибка: модификатор 'static' не может появиться в члене типа.

2. Решение

Решение обеих проблем (1.1 и 1.2) состоит в том, чтобы разрешить модификатор abstract в объявлениях статических методов в абстрактных классах и модификатор static в интерфейсах.

3. Реализация JS

Реализация этой функции в JavaScript должна быть аналогична реализации интерфейсов, абстрактных и статических методов.

Это означает, что:

  1. Объявление абстрактных статических методов в абстрактном классе не должно влиять на представление абстрактного класса в коде JavaScript.
  2. Объявление статических методов в интерфейсе не должно влиять на представление интерфейса в JavaScript-коде (его нет).

Например, этот код TypeScript (пример 4):

interface Serializable {
    serialize(): string;
    static deserialize(serializedValue: string): Serializable;
}

abstract class AbstractParentClass {
    public abstract static getSomeClassDependentValue(): string;
}

class FirstChildClass extends AbstractParentClass {

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

class SecondChildClass extends AbstractParentClass implements Serializable {

    public serialize(): string {
        var serialisedValue: string;
        // serialization of this
        return serialisedValue;
    }

    public static deserialize(serializedValue: string): SecondChildClass {
        var instance = new SecondChildClass();
        // deserialization
        return instance;
    }

    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class SecondChildClass';
    }
}

должен быть скомпилирован в этот код JS:

var AbstractParentClass = (function () {
    function AbstractParentClass() {
    }
    return AbstractParentClass;
}());

var FirstChildClass = (function (_super) {
    __extends(FirstChildClass, _super);
    function FirstChildClass() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    FirstChildClass.getSomeClassDependentValue = function () {
        return 'Some class-dependent value of class FirstChildClass';
    };
    return FirstChildClass;
}(AbstractParentClass));

var SecondChildClass = (function (_super) {
    __extends(SecondChildClass, _super);
    function SecondChildClass() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    SecondChildClass.prototype.serialize = function () {
        var serialisedValue;
        // serialization of this
        return serialisedValue;
    };
    SecondChildClass.deserialize = function (serializedValue) {
        var instance = new SecondChildClass();
        // deserialization
        return instance;
    };
    SecondChildClass.getSomeClassDependentValue = function () {
        return 'Some class-dependent value of class SecondChildClass';
    };
    return SecondChildClass;
}(AbstractParentClass));

4. Соответствующие моменты

  • Объявление абстрактного статического метода абстрактного класса должно быть помечено модификатором abstract static
  • Реализация абстрактного статического метода абстрактного класса должна быть отмечена модификатором abstract static или static
  • Объявление статического метода интерфейса должно быть помечено модификатором static
  • Реализация статического метода интерфейса должна быть отмечена модификатором static

Все остальные свойства модификатора abstract static должны быть унаследованы от свойств модификаторов abstract и static .

Все остальные свойства модификатора метода интерфейса static должны быть унаследованы от методов интерфейса и свойств модификатора static .

5. Контрольный список языковых функций

  • Синтаксический

    • _Какова грамматика этой функции?_ - Грамматика этой функции представляет собой модификатор abstract static метода абстрактного класса и модификатор static метода интерфейса.

    • _Есть ли какие-либо последствия для обратной совместимости JavaScript? Если да, то достаточно ли они смягчены?_ – Нет никаких последствий для обратной совместимости JavaScript.

    • _Этот синтаксис мешает изменениям ES6 или правдоподобным изменениям ES7?_ — Этот синтаксис не мешает изменениям ES6 или правдоподобным изменениям ES7.

  • Семантический

    • _В чем ошибка предложенной фичи?_ - Теперь есть ошибки компиляции модификатора abstract static метода абстрактного класса и модификатора static метода интерфейса.

    • _Как эта функция влияет на отношения подтипа, супертипа, идентичности и назначаемости?_ — Эта функция не влияет на отношения подтипа, супертипа, идентичности и назначаемости.

    • _Как функция взаимодействует с дженериками?_ — Функция не взаимодействует с дженериками.

  • Испускают

    • _Как эта функция влияет на генерацию JavaScript?_ — Эта функция не влияет на генерацию JavaScript.

    • _Корректно ли это испускает при наличии переменных типа any?_ - Да.

    • _Каковы воздействия на файл декларации (.d.ts)?_ — Нет влияния на файл декларации.

    • _Хорошо ли работает эта функция с внешними модулями?_ - Да.

  • Совместимость

    • _Это критическое изменение по сравнению с компилятором 1.0?_ - Вероятно, да, компилятор 1.0 не сможет скомпилировать код, реализующий эту функцию.

    • _Это критическое изменение поведения JavaScript?_ - Нет .

    • _Является ли это несовместимой реализацией будущей функции JavaScript (т.е. ES6/ES7/более поздней версии)?_ - Нет .

  • Другой

    • _Можно ли реализовать эту функцию без негативного влияния на производительность компилятора?_ - Вероятно, да.

    • _Какое влияние это оказывает на сценарии инструментов, такие как завершение членов и помощь по подписи в редакторах?_ — Вероятно, это не имеет никакого влияния такого типа.

Awaiting More Feedback Suggestion

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

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

Я хочу принудительно реализовать метод статической десериализации в подклассах Serializable.
Есть ли обходной путь для реализации такого поведения?

Все 98 Комментарий

Статические методы интерфейса обычно не имеют смысла, см. #13462.

Отложите это, пока мы не услышим больше отзывов об этом.

Главный вопрос, который у нас возник при рассмотрении этого вопроса: кому разрешено вызывать метод abstract static ? Предположительно, вы не можете вызывать AbstractParentClass.getSomeClassDependentValue напрямую. Но можете ли вы вызвать метод для выражения типа AbstractParentClass ? Если да, то почему это должно быть разрешено? Если нет, то какая польза от этой функции?

Статические методы интерфейса обычно не имеют смысла, см. #13462.

В обсуждении #13462 я не понял, почему методы интерфейса static бессмысленны. Я только видел, что их функциональность может быть реализована другими средствами (что доказывает, что они не бессмысленны).

С практической точки зрения интерфейс представляет собой некую спецификацию — определенный набор методов, обязательных для их реализации в классе, реализующем этот интерфейс. Интерфейс не только определяет функциональность, предоставляемую объектом, но и является своего рода контрактом.

Поэтому я не вижу никаких логических причин, по которым class может иметь метод static , а interface — нет.

Если придерживаться точки зрения, что все, что уже может быть реализовано в языке, не нуждается ни в каких доработках синтаксиса и прочего (а именно этот аргумент был одним из основных моментов в обсуждении #13462), то руководствуясь этим с точки зрения, мы можем решить, что цикл while является избыточным, поскольку его можно реализовать, используя вместе for и if . Но мы не собираемся отказываться от while .

Главный вопрос, который у нас возник при рассмотрении этого вопроса: кому разрешено вызывать метод abstract static ? Предположительно, вы не можете вызывать AbstractParentClass.getSomeClassDependentValue напрямую. Но можете ли вы вызвать метод для выражения типа AbstractParentClass ? Если да, то почему это должно быть разрешено? Если нет, то какая польза от этой функции?

Хороший вопрос. Поскольку вы рассматривали этот вопрос, не могли бы вы поделиться своими соображениями по этому поводу?

Мне приходит в голову только то, что на уровне компилятора случай прямого вызова AbstractParentClass.getSomeClassDependentValue не будет отслеживаться (потому что его нельзя отследить), и будет возникать ошибка выполнения JS. Но я не уверен, соответствует ли это идеологии TypeScript.

Я только видел, что их функциональность может быть реализована другими средствами (что доказывает, что они не бессмысленны).

То, что что-то реализуемо, не означает, что оно имеет смысл. 😉

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

Я хочу принудительно реализовать метод статической десериализации в подклассах Serializable.
Есть ли обходной путь для реализации такого поведения?

Каков последний взгляд на это? Я только что попытался написать абстрактное статическое свойство абстрактного класса и был искренне удивлен, когда это было запрещено.

Что сказал @patryk-zielinski93. Исходя из нескольких лет проектов на PHP, которые мы конвертируем в TS, нам ДЕЙСТВИТЕЛЬНО нужны статические методы в интерфейсах.

Очень распространенный вариант использования, с которым это поможет, — это компоненты React, которые представляют собой классы со статическими свойствами, такими как displayName , propTypes и defaultProps .

Из-за этого ограничения типы для React в настоящее время включают два типа: класс $# Component ComponentClass , включающий функцию конструктора и статические свойства.

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

Пример без ComponentClass : статические свойства игнорируются

import React, { Component, ComponentClass } from 'react';

type Props = { name: string };

{
  class ReactComponent extends Component<Props, any> {
    // expected error, but got none: displayName should be a string
    static displayName = 1
    // expected error, but got none: defaultProps.name should be a string
    static defaultProps = { name: 1 }
  };
}

Пример с ComponentClass : статические свойства проверяются на тип

{
  // error: displayName should be a string
  // error: defaultProps.name should be a string
  const ReactComponent: ComponentClass<Props> = class extends Component<Props, any> {
    static displayName = 1
    static defaultProps = { name: 1 }
  };
}

Я подозреваю, что многие люди в настоящее время не используют ComponentClass , не зная, что их статические свойства не проверяются на тип.

Связанная проблема: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16967

Есть ли прогресс в этом? Или какой-либо обходной путь для конструкторов абстрактных классов?
Вот пример:

abstract class Model {
    abstract static fromString(value: string): Model
}

class Animal extends Model {
    constructor(public name: string, public weight: number) {}

    static fromString(value: string): Animal {
        return new Animal(...JSON.parse(value))
    }
}

@робослоун

class Animal {
    static fromString(value: string): Animal {
        return new Animal();
    }
}

function useModel<T>(model: { fromString(value: string): T }): T {
    return model.fromString("");
}

useModel(Animal); // Works!

Согласитесь, что это чрезвычайно мощная и полезная функция. На мой взгляд, именно эта особенность делает классы «гражданами первого сорта». Наследование классов/статических методов может и имеет смысл, особенно для шаблона фабрики статических методов, который несколько раз упоминался здесь другими авторами. Этот шаблон особенно полезен для десериализации, которая часто выполняется в TypeScript. Например, имеет смысл определить интерфейс, который предоставляет контракт, в котором говорится, что все реализуемые типы могут быть созданы из JSON.

Отказ от абстрактных статических фабричных методов требует, чтобы разработчик вместо этого создавал абстрактные фабричные классы, без необходимости удваивая количество определений классов. И, как указывали другие авторы, это мощная и успешная функция, реализованная на других языках, таких как PHP и Python.

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

  1. TS не нуждается в этой функции, потому что вы все равно можете выполнить то, что пытаетесь сделать, с помощью других средств (что является действительным аргументом только в том случае, если вы приводите пример очень объективно лучшего способа сделать что-то, что я видел очень мало)
  2. То, что мы можем, не означает, что мы должны. Отлично: но люди публикуют конкретные примеры того, как это было бы полезно/выгодно. Я не понимаю, как это может повредить, чтобы позволить это.

Еще один простой вариант использования: (идеальный способ, который не работает)

import {map} from 'lodash';

export abstract class BaseListModel {
  abstract static get instance_type();

  items: any[];

  constructor(items?: any[]) {
    this.items = map(items, (item) => { return new this.constructor.instance_type(item) });
  }

  get length() { return this.items.length; }
}

export class QuestionList extends BaseListModel {
  static get instance_type() { return Question }
}

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

Я был очень рад использовать настоящие абстрактные классы в языке после столь долгого пребывания в ruby/javascript, только чтобы в конечном итоге встревожиться из-за ограничений реализации. Приведенный выше пример был только моим первым примером, с которым я столкнулся, хотя я могу вспомнить многих другие варианты использования, где это было бы полезно. В основном, просто как средство создания простых DSL/конфигураций как части статического интерфейса, гарантируя, что класс указывает объект значений по умолчанию или что-то еще. - И вы можете подумать, ну вот для чего нужны интерфейсы. Но для чего-то простого, подобного этому, это на самом деле не имеет смысла и приводит только к тому, что вещи становятся более сложными, чем они должны быть (подкласс должен расширять абстрактный класс и реализовывать некоторый интерфейс, усложняет именование вещей, так далее).

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

class Action {
  constructor(public type='') {}
}

class AddAppleAction extends Action {
  static create(apple: Apple) {
    return new this(apple);
  }
  constructor(public apple: Apple) {
    super('add/apple');
  }
}

class AddPearAction extends Action {
  static create(pear: Pear) {
    return new this(pear);
  }

  constructor(public pear: Pear) {
    super('add/pear');
  }
}

const ActionCreators = {
  addApple: AddAppleAction
  addPear: AddPearAction
};

const getActionCreators = <T extends Action>(map: { [key: string]: T }) => {
  return Object.entries(map).reduce((creators, [key, ActionClass]) => ({
    ...creators,
    // To have this function run properly,
    // I need to guarantee that each ActionClass (subclass of Action) has a static create method defined.
    // This is where I want to use abstract class or interface to enforce this logic.
    // I have some work around to achieve this by using interface and class decorator, but it feels tricky and buggy. A native support for abstract class method would be really helpful here.
    [key]: ActionClass.create.bind(ActionClass)
  }), {});
};

Надеюсь, это может объяснить мои требования. Спасибо.

Для тех, кто ищет обходной путь , вы можете использовать этот декоратор:

class myClass {
    public classProp: string;
}

interface myConstructor {
    new(): myClass;

    public readonly staticProp: string;
}

function StaticImplements <T>() {
    return (constructor: T) => { };
}

<strong i="7">@StaticImplements</strong> <myConstructor>()
class myClass implements myClass {}
const getActionCreators = <T extends Action>(map: { [key: string]: {new () => T, create() : T}}) => {

поскольку мы вызываем через параметр типа, фактический базовый класс с его гипотетическим фабричным методом abstract static никогда не будет задействован. Типы подклассов связаны структурно, когда создается экземпляр параметра типа.

Чего я не вижу в этом обсуждении, так это случаев, когда реализация вызывается посредством типа базового класса, а не какого-то синтезированного типа.

Также уместно учитывать, что, в отличие от таких языков, как C#, где члены abstract на самом деле _переопределяются_ их реализациями в производных классах, реализации членов JavaScript, экземплярные или иные, никогда не переопределяют унаследованные числа, а скорее _затеняют_ их.

Мои 5 центов здесь, с точки зрения пользователя:

В случае с интерфейсами следует разрешить модификатор _static_

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

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

На мой взгляд, это неправильно и мешает.

Есть ли какая-либо техническая подоплека, делающая эту языковую функцию трудной или невозможной для реализации?

ваше здоровье

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

Классы имеют два интерфейса, два контракта реализации, и от этого никуда не деться.
Есть причины, по которым такие языки, как C#, также не имеют статических членов для интерфейсов. Логически интерфейсы — это общедоступная поверхность объекта. Интерфейс описывает общедоступную поверхность объекта. Это означает, что он не содержит ничего, чего там нет. В экземплярах классов статические методы отсутствуют. Они существуют только в функции класса/конструктора, поэтому их следует описывать только в этом интерфейсе.

В экземплярах классов статические методы отсутствуют.

Вы можете уточнить это? Это не С#

Они существуют только в функции класса/конструктора, поэтому их следует описывать только в этом интерфейсе.

Что мы получили это, и это то, что мы хотим изменить.

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

Я полностью согласен с этим. См. пример интерфейса Json

Привет @kitsonk , не могли бы вы подробнее рассказать о:

Классы имеют два интерфейса, два контракта реализации, и от этого никуда не деться.

Я не понял эту часть.

Логически интерфейсы — это общедоступная поверхность объекта. Интерфейс описывает общедоступную поверхность объекта. Это означает, что он не содержит ничего, чего там нет.

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

В экземплярах классов статические методы отсутствуют. Они существуют только в функции класса/конструктора, поэтому их следует описывать только в этом интерфейсе.

Не уверен, что понял это правильно, понятно, что это говорите, но не то, почему вы это говорите. Я могу объяснить, почему я считаю все еще актуальным мое утверждение. Я все время передаю интерфейсы в качестве параметров своим методам, это означает, что у меня есть доступ к методам интерфейса, обратите внимание, что я использую здесь интерфейсы не как способ определения структуры данных, а для определения конкретных объектов, которые создаются/гидратируются в другом месте. . Итак, когда у меня есть:

fetchData(account: SalesRepresentativeInterface): Observable<Array<AccountModel>> {
    // Method Body
}

В теле этого метода я действительно могу использовать методы account . Что я хотел бы сделать, так это иметь возможность использовать статические методы из SalesRepresentativeInterface , которые уже были принудительно реализованы в любом классе, который я получаю в account . Возможно, у меня очень упрощенное или совершенно неправильное представление о том, как использовать эту функцию.

Я думаю, что использование модификатора static позволит мне сделать что-то вроде: SalesRepresentativeInterface.staticMethodCall()

Я ошибся ?

ваше здоровье

@davidmpaz : ваш синтаксис не совсем правильный, но близкий. Вот пример шаблона использования:

interface JSONSerializable {
  static fromJSON(json: any): JSONSerializable;
  toJSON(): any;
}

function makeInstance<T extends JSONSerializable>(cls: typeof T): T {
  return cls.fromJSON({});
}

class ImplementsJSONSerializable implements JSONSerializable {
  constructor(private json: any) {
  }
  static fromJSON(json: any): ImplementsJSONSerializable {
    return new ImplementsJSONSerializable(json);
  }
  toJSON(): any {
    return this.json;
  }
}

// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable);

К сожалению, чтобы это работало, нам нужны две функции TypeScript: (1) статические методы в интерфейсах и абстрактные статические методы; (2) возможность использовать typeof в качестве подсказки типа с универсальными классами.

@davidmpaz

Я не понял эту часть.

Классы имеют два интерфейса. Функция-конструктор и прототип экземпляра. Ключевое слово class по сути является сахаром для этого.

interface Foo {
  bar(): void;
}

interface FooConstructor {
  new (): Foo;
  prototype: Foo;
  baz(): void;
}

declare const Foo: FooConstructor;

const foo = new Foo();
foo.bar();  // instance method
Foo.baz(); // static method

@jimmykane

Вы можете уточнить это? Это не С#

class Foo {
    bar() {}
    static baz() {}
}

const foo = new Foo();

foo.bar();
Foo.baz();

В экземплярах Foo нет .baz() #$ . .baz() существует только в конструкторе.

С точки зрения типа вы можете ссылаться/извлекать эти два интерфейса. Foo относится к общедоступному интерфейсу экземпляра, а typeof Foo относится к общедоступному интерфейсу конструктора (который будет включать статические методы).

Чтобы продолжить объяснение @kitsonk , вот приведенный выше пример, переписанный для достижения того, чего вы хотите:

interface JSONSerializable <T>{
    fromJSON(json: any): T;
}

function makeInstance<T>(cls: JSONSerializable<T>): T {
    return cls.fromJSON({});
}

class ImplementsJSONSerializable {
    constructor (private json: any) {
    }
    static fromJSON(json: any): ImplementsJSONSerializable {
        return new ImplementsJSONSerializable(json);
    }
    toJSON(): any {
        return this.json;
    }
}

// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializable); 

Обратите внимание, что:

  • Предложение implements на самом деле не нужно, каждый раз, когда вы используете класс, выполняется структурная проверка, и класс будет проверен на соответствие требуемому интерфейсу.
  • вам не нужно typeof для получения типа конструктора, просто убедитесь, что ваши T — это то, что вы собираетесь захватить

@mhegazy : вам пришлось удалить мой вызов toJSON в интерфейсе JSONSerializable. Хотя мой простой пример функции makeInstance вызывает только fromJSON , очень часто требуется создать экземпляр объекта, а затем использовать его. Вы лишили меня возможности обращаться к тому, что возвращает makeInstance , потому что я на самом деле не знаю, что такое T внутри makeInstance (особенно важно, если makeInstance — это метод класса, используемый внутри для создания экземпляра, а затем его использования). Конечно, я мог бы сделать это:

interface JSONSerializer {
  toJSON(): any;
}

interface JSONSerializable <T extends JSONSerializer> {
  fromJSON(json: any): T;
}

function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
  return cls.fromJSON({});
}

class ImplementsJSONSerializer implements JSONSerializer {
  constructor (private json: any) {
  }
  static fromJSON(json: any): ImplementsJSONSerializer {
    return new ImplementsJSONSerializer(json);
  }
  toJSON(): any {
    return this.json;
  }
}

// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);

И теперь я знаю, что у моего T будут все методы, доступные на JSONSerializer . Но это слишком многословно и трудно рассуждать (подождите, вы передаете ImplementsJSONSerializer , но это не JSONSerializable , не так ли? Подождите, вы печатаете утиным шрифтом ??) . Более простая для понимания версия:

interface JSONSerializer {
  toJSON(): any;
}

interface JSONSerializable <T extends JSONSerializer> {
  fromJSON(json: any): T;
}

function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
  return cls.fromJSON({});
}

class ImplementsJSONSerializer implements JSONSerializer {
  constructor (private json: any) {
  }
  toJSON(): any {
    return this.json;
  }
}

class ImplementsJSONSerializable implements JSONSerializable<ImplementsJSONSerializer> {
  fromJSON(json: any): ImplementsJSONSerializer {
    return new ImplementsJSONSerializer(json);
  }

}

// returns an instance of ImplementsJSONSerializable
makeInstance(new ImplementsJSONSerializable());

Но, как я указал в предыдущем комментарии, теперь мы должны создать фабричный класс для каждого класса, который мы хотим создать, что еще более многословно, чем пример с утиным вводом. Это, безусловно, работающий шаблон, который все время делается на Java (и я предполагаю, что и на C#). Но это излишне многословно и дублирует. Весь этот шаблон исчезает, если в интерфейсах разрешены статические методы, а в абстрактных классах разрешены абстрактные статические методы.

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

другими словами, у вас может быть один класс со статическим fromJSON и чьи экземпляры имеют toJSON :

interface JSONSerializer {
    toJSON(): any;
}

interface JSONSerializable<T extends JSONSerializer> {
    fromJSON(json: any): T;
}

function makeInstance<T extends JSONSerializer>(cls: JSONSerializable<T>): T {
    return cls.fromJSON({});
}

class ImplementsJSONSerializer {
    constructor (private json: any) {
    }
    static fromJSON(json: any): ImplementsJSONSerializer {
        return new ImplementsJSONSerializer(json);
    }
    toJSON(): any {
        return this.json;
    }
}


// returns an instance of ImplementsJSONSerializable
makeInstance(ImplementsJSONSerializer);

@mhegazy Я уже указал на это как на жизнеспособный вариант. См. мой первый пример «утиного набора». Я утверждал, что это излишне многословно и трудно рассуждать. Затем я утверждал, что версия, о которой легче рассуждать (фабричные классы), еще более многословна.

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

Но, на мой взгляд, самый чистый и простой способ реализации этого исключительно распространенного шаблона — это использование абстрактных статических методов. Тем из нас, кто полюбил эту функцию в других языках (Python, PHP), не хватает ее в TypeScript. Спасибо за ваше время.

Затем я утверждал, что версия, о которой легче рассуждать (фабричные классы)

не уверен, что согласен с тем, что об этом легче рассуждать.

Но, на мой взгляд, самый чистый и простой способ реализации этого исключительно распространенного шаблона — это использование абстрактных статических методов. Тем из нас, кто полюбил эту функцию в других языках (Python, PHP), не хватает ее в TypeScript.

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

@kitsonk верно, я пропустил это.

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

Итак, теперь мне нужно создать фабрику с дженериками для отдыха, и даже тогда у меня нет проверок времени компиляции. Вы знаете, избили цель здесь.

Сначала я сказал: «Нет статики для интерфейсов, поэтому давайте придерживаться абстрактного класса, НО даже это будет грязно, так как теперь мне нужно везде менять тип кода с интерфейса на абстрактный класс, чтобы средство проверки распознало методы, которые я ищу .

Здесь:

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
}

export abstract class AbstractLocationModule implements LocationModuleInterface {
  abstract readonly name: string;

  abstract getRecreationOptions(): ModuleRecreationOptions;

  abstract static recreateFromOptions(options: ModuleRecreationOptions): AbstractLocationModule;
}

И тут я натыкаюсь на хорошо, статичное не может быть с абстрактным. И это не может быть в интерфейсе.

Ребята, серьезно, разве мы не защищаемся, не внедряя это только для того, чтобы не реализовать это?

Я страстно люблю TypeScript. Как сумасшедший, я имею в виду. Но всегда есть но.

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

Архитектура уже полна паттернов, крутых решений и т.д., но некоторые из них просто для преодоления проблем с использованием таких простых вещей, как статический метод интерфейса.

Моя фабрика должна будет проверять наличие статического метода во время выполнения. Как так?

Или еще лучше:

export interface LocationModuleInterface {
  readonly name: string;

  getRecreationOptions(): ModuleRecreationOptions;
  dontForgetToHaveStaticMethodForRecreation();
}

Если вы используете объект класса полиморфно, что является единственной причиной для реализации интерфейса, не имеет значения, синтаксически ссылается ли сам класс на интерфейс(ы), определяющий требования к статической части класса. потому что он будет проверен на сайте использования и выдаст ошибку времени разработки, если необходимый интерфейс (есть) не реализован.

@малина-кирн

См. мой первый пример «утиного набора».

Эта проблема не имеет отношения к тому, используется ли утиная типизация. TypeScript — это утиный тип.

@aluanhaddad Ваш вывод неверен. Я могу понять вашу точку зрения, что основная функция implements ISomeInterface предназначена для полиморфного использования объекта класса. Однако МОЖЕТ иметь значение, ссылается ли класс на интерфейсы, которые описывают статическую форму объекта класса.

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

interface IComponent<TProps> {
    render(): JSX.Element;
}

type ComponentStaticDisplayName = 'One' | 'Two' | 'Three' | 'Four';
interface IComponentStatic<TProps> {
    defaultProps?: Partial<TProps>;
    displayName: ComponentStaticDisplayName;
    hasBeenValidated: 'Yes' | 'No';
    isFlammable: 'Yes' | 'No';
    isRadioactive: 'Yes' | 'No';
    willGoBoom: 'Yes' | 'No';
}

interface IComponentClass<TProps> extends IComponentStatic<TProps> {
    new (): IComponent<TProps> & IComponentStatic<TProps>;
}

function validateComponentStaticAtRuntime<TProps>(ComponentStatic: IComponentStatic<TProps>, values: any): void {
    if(ComponentStatic.isFlammable === 'Yes') {
        // something
    } else if(ComponentStatic.isFlammable === 'No') {
        // something else
    }
}

// This works, we've explicitly described the object using an interface
const componentStaticInstance: IComponentStatic<any> = {
    displayName: 'One',
    hasBeenValidated: 'No',
    isFlammable: 'Yes',
    isRadioactive: 'No',
    willGoBoom: 'Yes'
};

// Also works
validateComponentStaticAtRuntime(componentStaticInstance, {});

class ExampleComponent1 implements IComponent<any> {
    public render(): JSX.Element { return null; }

    public static displayName = 'One'; // inferred as type string
    public static hasBeenValidated = 'No'; // ditto ...
    public static isFlammable = 'Yes';
    public static isRadioactive = 'No';
    public static willGoBoom = 'Yes';
}

// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent1, {});

class ExampleComponent2 implements IComponent<any> {
    public render(): JSX.Element { return null; }

    public static displayName = 'One';
    public static hasBeenValidated = 'No';
    public static isFlammable = 'Yes';
    public static isRadioactive = 'No';
    public static willGoBoom = 'Yes';
}

// Error: " ... not assignable to type IComponentStatic<..> ... "
validateComponentStaticAtRuntime(ExampleComponent2, {});

В приведенном выше примере требуется явное объявление типа; отсутствие описания формы объекта класса (статическая сторона) приводит к ошибке времени разработки на сайте использования, даже если фактические значения соответствуют ожидаемой форме.

Кроме того, отсутствие способа сослаться на интерфейс(ы), описывающие статическую сторону, оставляет нам единственный вариант – явно объявить тип для каждого отдельного члена; а затем повторить это на каждом уроке.

Опираясь на мой предыдущий пост, я считаю, что модификатор static в интерфейсах — действительно хорошее решение для некоторых случаев использования, которые необходимо разрешить. Прошу прощения за то, что этот комментарий разросся, но есть многое, что я хочу проиллюстрировать.

1) Бывают случаи, когда нам нужно иметь возможность явно описать форму статической стороны объекта класса, например, в моем примере выше.
2) Бывают случаи, когда проверка формы на сайте определения очень желательна, и такие случаи, как в примере @OliverJAsh , где сайт использования находится во внешнем коде, который не будет проверяться, и вам необходимо проверить форму на сайте определения .

Что касается номера 2, многие сообщения, которые я читал, предлагают проверку формы, поскольку сайта использования более чем достаточно. Но в случаях, когда место использования находится в модуле галактики далеко-далеко... или когда место использования находится во внешнем месте, которое не будет проверено, это, очевидно, не так.

В других сообщениях предлагается #workarounds проверить форму на сайте определения. Хотя эти ужасные обходные пути позволят вам проверить форму на сайте определения (в некоторых случаях), есть проблемы:

  • Они не решают номер 1 выше, вам все равно нужно явно объявлять тип для каждого члена в каждом классе.
  • Во многих случаях им не хватает ясности, элегантности и удобочитаемости. Их не всегда легко использовать, они не интуитивно понятны, и может потребоваться некоторое время, чтобы понять, что они возможны.
  • Они не работают во всех случаях, и найти #workaroundsToMakeTheWorkaroundsWork не весело... или продуктивно... но в основном не весело.

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

// (at least) two separate interfaces
interface ISomeComponent { ... }
interface ISomeComponentClass { ... }

// confusing workaround with extra work to use, not very intuitive, etc
export const SomeComponent: ISomeComponentClass =
class SomeComponent extends Component<IProps, IState> implements ISomeComponent {
...
}

Теперь попробуйте использовать этот обходной путь с добавленным абстрактным классом.

interface ISomeComponent { ... }
// need to separate the static type from the class type, because the abstract class won't satisfy
// the shape check for new(): ... 
interface ISomeComponentStatic { ... }
interface ISomeComponentClass { 
    new (): ISomeComponent & ISomeComponentStatic;
}

// I AM ERROR.    ... cannot find name abstract
export const SomeComponentBase: ISomeComponentStatic =
abstract class SomeComponentBase extends Component<IProps, IState> implements ISomeComponent {
...
}

export const SomeComponent: ISomeComponentClass =
class extends SomeComponentBase { ... }

Мы, я думаю, тоже обойдем это... вздох

...

abstract class SomeComponentBaseClass extends Component<IProps, IState> implements ISomeComponent { 
   ... 
}

// Not as nice since we're now below the actual definition site and it's a little more verbose
// but hey at least were in the same module and we can check the shape if the use site is in
// external code...
// We now have to decide if we'll use this work around for all classes or only the abstract ones
// and instead mix and match workaround syntax
export const SomeComponentBase: ISomeComponentStatic = SomeComponentBaseClass;

Но подождите, есть еще! Давайте посмотрим, что произойдет, если мы используем дженерики....

interface IComponent<TProps extends A> { ... }
interface IComponentStatic<TProps extends A> { ... }

interface IComponentClass<TProps extends A, TOptions extends B> extends IComponentStatic<TProps> {
    new (options: TOptions): IComponent<TProps> & IComponentStatic<TProps>;
}

abstract class SomeComponentBaseClass<TProps extends A, TOptions extends B> extends Component<TProps, IState> implements IComponent<TProps> {
...
}

// Ruh Roh Raggy: "Generic type .... requires 2 type argument(s) ..."
export const SomeComponentBase: IComponentStatic = SomeComponentBaseClass;


// "....  requires 1 type argument(s) ... "    OH NO, We need a different workaround
export const SomeComponent: IComponentStatic =
class extends SomeComponentBase<TProps extends A, ISomeComponentOptions> {
...
}

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

Если вы используете site is external, убедите сообщество/сопровождающих принять модификатор static для членов интерфейса. А пока вам понадобится другой обходной путь. Его нет на сайте определений, но я считаю, что для достижения этой цели можно использовать модифицированную версию обходного пути, описанного в #8328. См. обходной путь в комментарии от @mhegazy от 16 мая 2016 г., любезно предоставленного @RyanCavanaugh.

Простите меня, если я упускаю ключевые моменты. Мое понимание нерешительности в отношении поддержки static для членов интерфейса в первую очередь связано с неприязнью к использованию одного и того же ключевого слова interface+ implements для описания как формы конструктора, так и формы экземпляра.

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

Но нерешительность в данном случае кажется мне желанием сохранить «концептуальную чистоту» за счет удобства использования. Еще раз извините, если эта аналогия неверна:

Это напомнило переход с Java на C# . Когда я впервые увидел код C# , делающий это
C# var something = "something"; if(something == "something") { ... }
в моей голове зазвенели тревожные звоночки, настал конец света, им нужно было использовать "something".Equals(something) и т.д.

== для справки равно! У вас есть отдельный .Equals(...) для сравнения строк...
и строковый литерал должен быть первым, чтобы вы не получили нулевую ссылку, вызывающую .Equals(...) для нулевой переменной...
и... и... гипервентиляция

А затем, после пары недель использования C# , я понял, насколько проще было использовать его из-за такого поведения. Несмотря на то, что это означало отказ от четкого различия между ними, это дает такое значительное улучшение удобства использования, что оно того стоит.

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

Я расскажу о abstract static в следующем комментарии....

Интерфейсы всегда требуют переопределения типов членов при внедрении классов. (вывод подписей членов в _явно_ реализующих классах является отдельной, пока не поддерживаемой функцией).

Причина, по которой вы получаете ошибки, не связана с этим предложением. Чтобы обойти это, вам нужно использовать модификатор readonly для реализации членов класса, статических или иных, которые должны быть литеральными типами; в противном случае будет подразумеваться string .

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

Так что abstract static ... Не стоит. Слишком много проблем.

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

Если сайт использования является внешним, то вам действительно нужны члены интерфейса static ... Извините, что подключил.... и спокойной ночи!

@aluanhaddad Верно, но даже если вывод подписей членов в классах с явной реализацией был поддерживаемой функцией, нам не хватает чистых способов объявления подписей членов для статической стороны класса.

Суть, которую я пытался выразить, заключалась в том, что у нас нет способов объявить explicitly ожидаемую структуру статической стороны класса, и есть случаи, когда это имеет значение (или будет иметь значение для поддержки функций).

В первую очередь я хотел опровергнуть эту часть вашего комментария: «не имеет значения, синтаксически ссылаются ли интерфейсы, которые определяют требования к статической стороне класса».

Я пытался использовать вывод типов в качестве примера того, почему это будет иметь значение для будущей поддержки.
Синтаксически ссылаясь на интерфейс здесь let something: ISomethingStatic = { isFlammable: 'Ys', isFlammable2: 'No' ... isFlammable100: 'No' } , нам не нужно явно объявлять тип как «Да» | «Нет» 100 раз.

Чтобы добиться того же вывода типа для статической части класса (в будущем), нам понадобится какой-то способ синтаксической ссылки на интерфейс (ы).

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

За счет некоторой многословности можно добиться приличного уровня безопасности типов:

type HasType<T, Q extends T> = Q;

interface IStatic<X, Y> {
    doWork: (input: X) => Y
}

type A_implments_IStatic<X, Y> = HasType<IStatic<X, Y>, typeof A>    // OK
type A_implments_IStatic2<X> = HasType<IStatic<X, number>, typeof A> // OK
type A_implments_IStatic3<X> = HasType<IStatic<number, X>, typeof A> // OK
class A<X, Y> {
    static doWork<T, U>(_input: T): U {
        return null!;
    }
}

type B_implments_IStatic = HasType<IStatic<number, string>, typeof B> // Error as expected
class B {
    static doWork(n: number) {
        return n + n;
    }
}

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

export abstract class Component {
  public abstract static READ_FROM(buffer: ByteBuffer): Component;
}

// I want to force my DeckComponent class to implement it's own static ReadFrom method
export class DeckComponent extends Component {
  public cardIds: number[];

  constructor(cardIds: number[]) {
    this.cardIds = cardIds;
  }

  public static READ_FROM(buffer: ByteBuffer): DeckComponent {
    const cardIds: number[] = [...];
    return new DeckComponent(cardIds);
  }
}

@RyanCavanaugh Я не думаю, что кто-то еще точно упомянул об этом (хотя я мог пропустить это в своем быстром прочтении), но в ответ на:

Главный вопрос, который у нас возник при рассмотрении этого вопроса: кому разрешено вызывать абстрактный статический метод? Предположительно, вы не можете вызывать AbstractParentClass.getSomeClassDependentValue напрямую. Но можете ли вы вызвать метод для выражения типа AbstractParentClass? Если да, то почему это должно быть разрешено? Если нет, то какая польза от этой функции?

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

Итак, я предлагаю дать T.constructor тип Function & {{ statics on T }} . Это позволило бы абстрактным классам, объявляющим поле abstract static foo , безопасно обращаться к нему через this.constructor.foo , не вызывая при этом проблем с несовместимостью конструкторов.

И даже если автоматическое добавление статики в T.constructor не реализовано, мы все равно сможем использовать свойства abstract static в базовом классе, объявив вручную "constructor": typeof AbstractParentClass .

Я думаю, что многие люди ожидают, что пример @patryk- zielinski93 должен работать. Вместо этого мы должны использовать контринтуитивные, многословные и загадочные обходные пути. Поскольку у нас уже есть классы с «синтаксическим сахаром» и статические члены, почему мы не можем иметь такой сахар в системе типов?

Вот моя боль:

abstract class Shape {
    className() {
        return (<typeof Shape>this.constructor).className;
    }
    abstract static readonly className: string; // How to achieve it?
}

class Polygon extends Shape {
    static readonly className = 'Polygon';
}

class Triangle extends Polygon {
    static readonly className = 'Triangle';
}

Может быть, мы могли бы вместо/параллельно ввести оговорку static implements ? например

interface Foo {
    bar(): number;
}

class Baz static implements Foo {
    public static bar() {
        return 4;
    }
}

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

( statically implements было бы лучше, но это вводит новое ключевое слово. Спорный вопрос, стоит ли оно того. Хотя это может быть контекстуальным.)

Как бы я ни пытался понять аргументы тех, кто скептически относится к предложению ОП, мне это не удалось.

Только один человек (https://github.com/Microsoft/TypeScript/issues/14600#issuecomment-379645122) ответил вам @RyanCavanaugh . Я переписал код OP, чтобы сделать его немного чище, а также проиллюстрировать ваш вопрос и мои ответы:

abstract class Base {
  abstract static foo() // ref#1
}

class Child1 extends Base {
  static foo() {...} // ref#2
}

class Child2 extends Base {
  static foo() {...}
}

Кому разрешено вызывать абстрактный статический метод? Предположительно, вы не можете напрямую вызывать Base.foo

Если вы имеете в виду ref # 1, то да, его никогда нельзя вызывать, потому что он абстрактен и даже не имеет тела.
Вам разрешено вызывать только ref#2.

Но можете ли вы вызвать метод для выражения типа Base?
Если да, то почему это должно быть разрешено?

function bar(baz:Base) { // "expression of type Base"
  baz.foo() // ref#3
}

function bar2(baz: typeof Base) { // expression of type Base.constructor
  baz.foo() // ref#4
}

ref#3 является ошибкой. Нет, вы не можете вызвать там "foo", потому что baz должен быть __экземпляром__ Child1 или Child2, а экземпляры не имеют статических методов.

ссылка № 4 (должна быть) правильной. Вы можете (должны иметь возможность) вызывать там статический метод foo, потому что baz должен быть конструктором Child1 или Child2, которые оба расширяют Base и, следовательно, должны реализовывать foo().

bar2(Child1) // ok
bar2(Child2) // ok

Вы можете представить себе такую ​​ситуацию:

bar2(Base) // ok, but ref#4 should be red-highlighted with compile error e.g. "Cannot call abstract  method." 
// Don't know if it's possible to implement in compiler, though. 
// If not, compiler simply should not raise any error and let JS to do it in runtime (Base.foo is not a function). 

Какая польза от этой функции?

См. ссылку № 4. Использование в том случае, когда мы не знаем, какой из дочерних классов мы получаем в качестве аргумента (это может быть Child1 или Child2), но мы хотим вызвать его статический метод и убедиться, что этот метод существует.
Я переписываю фреймворк node.js под названием AdonisJS. Он был написан на чистом JS, поэтому я трансформирую его в TS. Я не могу изменить работу кода, я только добавляю типы. Отсутствие этих функций меня очень огорчает.

ps В этом комментарии я для простоты написал только про абстрактные классы, а не упомянул интерфейсы. Но все, что я написал, применимо к интерфейсам. Просто замените абстрактный класс интерфейсом, и все будет правильно. Есть вероятность, что команда TS по какой-то причине (не знаю почему) не захочет реализовывать abstract static в abstract class , однако было бы неплохо реализовать только static слово в интерфейсах, я думаю, это сделало бы нас достаточно счастливыми.

pps Я отредактировал имена классов и методов в ваших вопросах, чтобы они соответствовали моему коду.

Пара мыслей, основанных на основных аргументах других скептиков:

"Нет, это не должно быть реализовано, но вы уже можете сделать это таким образом *пишет в 15 раз больше строк кода*".

Вот для чего создан синтаксический сахар. Добавим немного (сахара) в ТС. Но нет, мы лучше помучаем мозги разработчикам, пытающимся пробиться через декораторы и дюжину дженериков, вместо того, чтобы добавлять в интерфейсы одно простое static слово. Почему бы не рассматривать эту static как еще один сахар, облегчающий нашу жизнь? Точно так же, как ES6 добавляет class (что в наши дни становится повсеместным). Но нет, давайте будем кретинами и будем делать вещи старыми и «правильными» способами.

«Есть два интерфейса для классов js: для конструктора и экземпляра».

Хорошо, почему бы тогда не дать нам возможность сделать интерфейс для конструктора? Однако просто добавить это слово static намного проще и интуитивно понятнее.

И относительно этого:

Отложите это, пока мы не услышим больше отзывов об этом.

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

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

Возможный обходной путь?

export type Constructor<T> = new (...args: any[]) => T;

declare global {
  interface Function extends StaticInterface {
  }
}

export interface StaticInterface {
  builder: (this: Constructor<MyClass>) => MyClass
}

// TODO add decorator that adds "builder" implementation
export class MyClass {
  name = "Ayyy"
}

export class OtherClass {
  id = "Yaaa"
}

MyClass.builder() // ok
OtherClass.builder() // error

Предложение: статические члены в интерфейсах и типах и абстрактные члены в абстрактных классах, v2

Случаи применения

Типы, совместимые с Fantasyland

````машинопись
интерфейс Applicative extends Apply {статика (a: A): Applicative ; }

const of = >(c: C, a: A): new C => c.of(a); ````

Тип Serializable со статическим методом deserialize

````машинопись
интерфейс Сериализуемый {
статическая десериализация (s: строка): Serializable;

serialize(): string;

}
````

Контракты в целом

````машинопись
интерфейс Контракт {
статическое создание (x: число): контракт;
статический новый (x: номер): контракт;
}

const factory1 = (c: static Contract): Contract => c.create(Math.random());
const factory2 = (C: static Contract): Contract => new C(Math.random());
постоянная фабрика3 =(c: C): новый C => c.create(Math.random());

const c1 = factory1 (ContractImpl); // Contract
const c2 = factory2 (ContractImpl); // Contract
const c3 = factory3 (ContractImpl); // ContractImpl
````

Синтаксис

Статические методы и поля

````машинопись
интерфейс Сериализуемый {
статическая десериализация (s: строка): Serializable;
}

тип Сериализуемый = {
статическая десериализация (s: строка): Serializable;
};

абстрактный класс Сериализуемый {
статическая абстрактная десериализация (s: строка): Serializable;
}
````

Сигнатуры статического конструктора

typescript interface Contract { static new(): Contract; }

Продолжение: статические сигнатуры вызовов

Обсуждать:

Как могут быть выражены статические сигнатуры вызова?
Если мы выразим их, просто добавив модификатор static перед позывным,
как мы будем отличать их от метода экземпляра с именем 'static' ?
Можем ли мы использовать тот же обходной путь, что и для 'new' именованных методов?
В таком случае это определенно будет критическое изменение.

Оператор типа static

typescript const deserialize = (Class: static Serializable, s: string): Serializable => Class.deserialize(s);

Оператор типа new

typescript const deserialize = <C extends static Serializable>(Class: C, s: string): new C => Class.deserialize(s);

Внутренние слоты

Внутренний слот [[Static]]

«Статический» интерфейс типа будет храниться во внутреннем слоте [[Static]] :

````машинопись
// Тип
интерфейс Сериализуемый {
статическая десериализация (s: строка): Serializable;
сериализовать: строка;
}

// будет внутренне представлен как
// сериализуемый интерфейс {
// [[Статический]]: {
// [[Экземпляр]]: Serializable; // Смотри ниже.
// десериализовать(s:string): Serializable;
// };
// сериализовать(): строка;
// }
````

По умолчанию имеют тип never .

Внутренний слот [[Instance]]

Интерфейс типа «Экземпляр» будет сохранен
во внутреннем слоте [[Instance]] типа внутреннего слота [[Static]] .

По умолчанию имеют тип never .

Семантика

Без оператора static

Когда тип используется в качестве типа значения,
внутренний слот [[Static]] будет удален:

````машинопись
объявить const сериализуемым: Serializable;

тип T = сериализуемый тип;
// { сериализовать(): строка; }
````

Но сам тип остается, сохраняя внутренний слот [[Static]] :

typescript type T = Serializable; // { // [[Static]]: { // [[Instance]]: Serializable; // deserialize(s: string): Serializable; // }; // serialize(): string; // }

Назначаемость

Оба значения статических типов могут быть присвоены структурно идентичным
(кроме [[Static]] , конечно) и наоборот.

Оператор static

| Ассоциативность | Приоритет |
| :-----------: | :--------------------------------: |
| Право | IDK, но равен new |

Оператор типа static возвращает тип внутреннего слота [[Static]] типа.
Он чем-то похож на оператор типа typeof ,
но его аргумент должен быть типом, а не значением.

typescript type T = static Serializable; // { // [[Instance]]: Serializable; // deserialize(s: string): Serializable; // }

Оператор типа typeof также отбрасывает внутренний слот [[Instance]] :

````машинопись
объявить const SerializableImpl: static Serializable;

тип T = тип SerializableImpl;
// { deserialize(s: string): Serializable; }
````

Назначаемость

Оба значения экземплярно-осведомленных типов можно присвоить структурно идентичным
(кроме внутреннего слота [[Instance]] , конечно) единицы и наоборот.

Оператор new

| Ассоциативность | Приоритет |
| :-----------: | :------------------------------------------------: |
| Право | IDK, но равен static |

Операторы new возвращают тип внутреннего слота [[Instance]] типа.
Он эффективно обращает оператор static :

typescript type T = new static Serializable; // { // [[Static]]: { // [[Instance]]: Serializable; // deserialize(s: string): Serializable; // }; // serialize(): string; // }

Семантика extends / implements

Класс, реализующий интерфейс с нестандартным внутренним слотом [[Static]]
ДОЛЖЕН иметь совместимый внутренний слот [[Static]] .
Проверки совместимости должны быть такими же (или похожими)
регулярные (экземплярные) проверки совместимости.

````машинопись
класс SerializableImpl реализует Serializable {
статическая десериализация (s: строка): SerializableImpl {
// Здесь идет логика десериализации.
}

// ...other static members
// constructor
// ...other members

serialize(): string {
    //
}

}
````

СДЕЛАТЬ

  • [ ] Решите, какой синтаксис следует использовать для статических сигнатур вызовов.
    Возможно без ломаных изменений.
  • [ ] Существуют ли особые случаи с условными типами и оператором infer ?
  • [ ] Изменения в семантике неабстрактных членов класса. _Может сломаться._

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

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

import {Message} from "../protobuf";

declare module "../protobuf" {
    interface Message {
        static factory: (params: MessageParams) => Message
    }
}

Message.factory = function(params: MessageParams) {
    const message = new Message();
    //... set up properties
    return message;
}

export default Message;

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

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

Если вы хотите проверить экземпляр и статическую часть класса на интерфейсы, вы можете сделать это следующим образом:

interface C1Instance {
  // Instance properties ...

  // Prototype properties ...
}
interface C2Instance extends C1Instance {
  // Instance properties ...

  // Prototype properties ...
}

interface C1Constructor {
  new (): C1Instance;

  // Static properties ...
}
interface C2Constructor {
  new (): C2Instance;

  // Static properties ...
}

type C1 = C1Instance;
let C1: C1Constructor = class {};

type C2 = C2Instance;
let C2: C2Constructor = class extends C1 {};

let c1: C1 = new C1();
let c2: C2 = new C2();

Слишком многие из нас тратят на это слишком много часов своего и чужого времени. Почему это не вещь?!¿i!
Почему ответы представляют собой огромные обходные пути для чего-то, что должно быть читабельным, удобоваримым, и что-то однострочное, что просто. Я никоим образом не хочу, чтобы кто-то выяснял, что мой код пытается сделать с помощью какого-то хакерского обходного пути. Извините, что еще больше засоряю эту тему тем, что не очень ценно, но в настоящее время это огромная боль и пустая трата времени, поэтому я считаю это ценным для себя, других прямо сейчас и тех, кто приходит позже в поисках ответа.

Дело в том, что любой язык, как естественный, так и искусственный, должен естественным образом развиваться, чтобы быть эффективным и привлекательным для использования. В конце концов люди решили использовать сокращения в языке («хорошо» => «хорошо», «собираюсь» => «собираюсь»), изобрели новые нелепые слова, такие как «селфи» и «гугл», изменили правописание с помощью l33tspeak и вещи, и даже запретили некоторые слова, и, независимо от того, хотите вы их использовать или нет, все все равно понимают, что они означают, и некоторые из нас действительно используют их для достижения каких-то конкретных задач. И ни для того, ни для другого не может быть никакой веской причины, кроме эффективности определенных людей в определенных задачах, все зависит от количества людей, которые действительно используют их. Объем этого разговора ясно показывает, что многие люди могли бы использовать этот static abstract для каких-то чертовых соображений, которые у них есть. Я пришел сюда по той же причине, потому что я хотел реализовать Serializable , поэтому я попробовал все интуитивно понятные (для меня) способы сделать это, и ни один из них не работает. Поверьте мне, последнее, что я бы искал, это объяснение, почему мне не нужна эта функция и я должен пойти на что-то другое. Полтора года, Господи Иисусе! Бьюсь об заклад, где-то уже есть пиар, с тестами и прочим. Пожалуйста, сделайте это, и если есть определенный способ использования, который не рекомендуется, у нас есть tslint для этого.

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

class A {
  f() {
    console.log(this.constructor.x)
  }
}

class B extends A {
  static x = "b"
}

const b = new B
b.f() // logs "b"

Класс A должен иметь возможность указать, что ему требуется статический член x , потому что он использует его в методе f .

Какие-нибудь Новости ?
Функции действительно нужны 😄
1) static функции в интерфейсах
2) abstract static функции в абстрактных классах

хотя я ненавижу идею наличия статических интерфейсов, но для всех практических целей сегодня должно быть достаточно следующего:

type MyClass =  (new (text: string) => MyInterface) & { myStaticMethod(): string; }

который можно использовать как:

const MyClass: MyClass = class implements MyInterface {
   constructor(text: string) {}
   static myStaticMethod(): string { return ''; }
}

ОБНОВИТЬ:

подробнее об идее и живой пример :

// dynamic part
interface MyInterface {
    data: number[];
}
// static part
interface MyStaticInterface {
    myStaticMethod(): string;
}

// type of a class that implements both static and dynamic interfaces
type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface;

// way to make sure that given class implements both 
// static and dynamic interface
const MyClass: MyClass = class MyClass implements MyInterface {
   constructor(public data: number[]) {}
   static myStaticMethod(): string { return ''; }
}

// works just like a real class
const myInstance = new MyClass([]); // <-- works!
MyClass.myStaticMethod(); // <-- works!

// example of catching errors: bad static part
/*
type 'typeof MyBadClass1' is not assignable to type 'MyClass'.
  Type 'typeof MyBadClass1' is not assignable to type 'MyStaticInterface'.
    Property 'myStaticMethod' is missing in type 'typeof MyBadClass1'.
*/
const MyBadClass1: MyClass = class implements MyInterface {
   constructor(public data: number[]) {}
   static myNewStaticMethod(): string { return ''; }
}

// example of catching errors: bad dynamic part
/*
Type 'typeof MyBadClass2' is not assignable to type 'MyClass'.
  Type 'typeof MyBadClass2' is not assignable to type 'new (data: number[]) => MyInterface'.
    Type 'MyBadClass2' is not assignable to type 'MyInterface'.
      Property 'data' is missing in type 'MyBadClass2'.
*/
const MyBadClass2: MyClass = class implements MyInterface {
   constructor(public values: number[]) {}
   static myStaticMethod(): string { return ''; }
}

@aleksey-bykov это может быть не ошибка Typescript, но я не смог получить эти работающие декораторы компонентов Angular и их компилятор AoT.

@aleksey-bykov это умно, но все еще не работает для абстрактной статики. Если у вас есть какие-либо подклассы MyClass , они не применяются с проверкой типов. Также хуже, если у вас есть дженерики.

// no errors
class Thing extends MyClass {

}

Я действительно надеюсь, что команда TypeScript пересмотрит свою позицию по этому поводу, потому что создание пользовательских библиотек, требующих статических атрибутов, не имеет разумной реализации. У нас должна быть возможность иметь контракт, который требует, чтобы разработчики интерфейсов/расширители абстрактных классов имели статику.

@bbugh Я сомневаюсь в самом существовании обсуждаемой здесь проблемы, зачем вам все эти проблемы со статическими абстрактными унаследованными методами, если то же самое можно сделать с помощью экземпляров обычных классов?

class MyAbstractStaticClass {
    abstract static myStaticMethod(): void; // <-- wish we could
}
class MyStaticClass extends MyAbstractStaticClass {
    static myStaticMethod(): void {
         console.log('hi');
    }
}
MyStaticClass.myStaticMethod(); // <-- would be great

против

class MyAbstractNonStaticClass {
    abstract myAbstractNonStaticMethod(): void;
}
class MyNonStaticClass extends MyAbstractNonStaticClass {
    myNonStaticMethod(): void {
        console.log('hi again');
    }
}
new MyNonStaticClass().myNonStaticMethod(); // <-- works today

@aleksey-bykov Причин много. Например ( от @patryk-zielinski93):

abstract class Serializable {  
    abstract serialize (): Object;  
    abstract static deserialize (Object): Serializable;  
}  

Я хочу принудительно реализовать метод статической десериализации в подклассах Serializable.
Есть ли обходной путь для реализации такого поведения?

РЕДАКТИРОВАТЬ: я знаю, что вы также можете использовать deserialize в качестве конструктора, но классы могут иметь только 1 конструктор, что делает необходимыми фабричные методы. Людям нужен способ требовать фабричных методов в интерфейсах, что вполне логично.

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

class Deserializer {
     deserializeThis(...): Xxx {}
     deserializeThat(...): Yyy {}
}

в чем проблема?

@aleksey-bykov отдельный класс выглядит не так красиво

@aleksey-bykov, проблема в том, что существует более 1 класса, требующего сериализации, поэтому ваш подход заставляет создавать словарь сериализуемых объектов, который является антипаттерном уберкласса, и каждое изменение любого из них потребует редактирования в этом уберклассе, что делает поддержку кода неприятной. В то время как наличие сериализуемого интерфейса может привести к реализации любого заданного типа объекта, а также поддерживать полиморфное наследование, что является основной причиной, по которой люди этого хотят. Пожалуйста, не стройте домыслов, есть ли польза или нет, каждый должен иметь возможность иметь варианты и выбирать то, что выгодно для его собственного проекта.

@octaharon , имеющий класс, посвященный исключительно десериализации, полностью противоположен тому, что вы сказали, он несет единственную ответственность, потому что единственный раз, когда вы его меняете, это когда вы заботитесь о десериализации

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

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

@octaharon , имеющий класс, посвященный исключительно десериализации, полностью противоположен тому, что вы сказали, он несет единственную ответственность, потому что единственный раз, когда вы его меняете, это когда вы заботитесь о десериализации

за исключением того, что вам нужно изменить код в двух местах вместо одного, так как ваша (де) сериализация зависит от вашей структуры типа. Это не считается "одиночной ответственностью"

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

Я не понимаю, что ты говоришь. Класс, отвечающий за собственную (де)сериализацию, — это именно то, что мне нужно, и, пожалуйста, не говорите мне, правильно это или нет.

одиночная ответственность ничего не говорит о том, сколько файлов задействовано, это только говорит о том, что должна быть одна причина

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

и извините за банальность, но вопросы (и ответы) о преимуществах и практических вариантах использования - вот что побуждает это предложение функции реализовать

SOLID не был послан Всемогущим Богом на планшете, и даже если бы это было так, в его интерпретации есть определенная степень свободы. Вы можете быть сколь угодно идеалистичны, но сделайте мне одолжение: не распространяйте свои убеждения среди общества. Каждый имеет полное право использовать любой инструмент так, как он хочет, и нарушать все возможные правила, которые он знает (нож нельзя обвинять в убийстве). Что определяет качество инструмента, так это баланс между спросом на определенные функции и предложением их. И эта ветка показывает объем спроса. Если вам не нужна эта функция - не используйте ее. Я делаю. И куча людей здесь тоже нуждается в этом, и _у нас_ есть практический вариант использования, а вы говорите, что мы должны игнорировать его, по сути. Речь идет только о том, что важнее для сопровождающих — святые принципы (как бы они их ни понимали) или сообщество.

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

image

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

все сводилось к типичному: просто так, не смей

лично меня не волнует солид или oop, я просто перерос его давным-давно, вы подняли его, бросив аргумент «антишаблон сверхкласса», а затем поддержали «степень свободы его интерпретации»

единственная практическая причина, упомянутая во всем этом обсуждении, такова: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119

Очень распространенный вариант использования, с которым это поможет, — это компоненты React, которые представляют собой классы со статическими свойствами, такими как displayName, propTypes и defaultProps.

и несколько подобных сообщений https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -345496014

но это покрывается (new (...) => MyClass) & MyStaticInterface

это точный вариант использования и причина, по которой я здесь. Вы видите количество голосов? Как вы думаете, почему лично вам решать, что practical , а что нет? Practical — это то, что можно заменить на practice , и (на момент написания) 83 человека сочли бы эту функцию чертовски практичной. Пожалуйста, уважайте других и читайте ветку полностью, прежде чем начинать вырывать фразы из контекста и выставлять разные словечки. Что бы вы ни преодолели, это определенно не ваше эго.

Здравый смысл подсказывает, что практичные вещи решают проблемы, а непрактичные вещи те, которые возбуждают ваше чувство прекрасного, я уважаю других, но при всем этом уважении вопрос (по большей части риторический сейчас) все еще актуален: какую проблему призвано решить это предложение? решить данный (new (...) => MyClass) & MyStaticInterface для https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -308362119 и https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -289084844 просто причина

пожалуйста, не отвечайте

По той же причине, по которой я иногда использую объявления type для сокращения больших аннотаций, я думаю, что ключевое слово вроде abstract static было бы намного более читабельным, чем относительно более трудная для восприятия конструкция, такая как упомянутая как существующая. пример.

Кроме того, мы до сих пор не рассмотрели абстрактные классы?

Решение не использовать абстрактные классы, на мой взгляд, не является решением. Это обходной путь! Обход вокруг чего?

Я думаю, что этот запрос функции существует, потому что многие люди, включая инициатора запроса, обнаружили, что ожидаемая функция, такая как abstract static или static в интерфейсах, отсутствует.

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

Проблема здесь в том, что static имеет гораздо больше смысла.
Основываясь на возникшем интересе, можем ли мы провести менее пренебрежительное обсуждение?

Были ли какие-либо обновления по этому предложению? Любые аргументы, достойные рассмотрения, которые демонстрируют, почему мы не должны иметь static abstract и тому подобное?
Можем ли мы иметь больше предложений, которые показывают, почему это было бы полезно?

Возможно, нам нужно собраться и обобщить то, что обсуждалось, чтобы найти решение.

Есть два предложения, как я понимаю:

  1. интерфейсы и типы могут определять статические свойства и методы
interface ISerializable<T> { 
   static fromJson(json: string): T;
}
  1. абстрактные классы могут определять абстрактные статические методы
abstract class MyClass<T> implements ISerializable<T> {
   abstract static fromJson(json: string): T;
}

class MyOtherClass extends MyClass<any> {
  static fromJson(json: string) {
  // unique implementation
  }
}

Что касается первого предложения, то технически есть обходной путь! Что не очень хорошо, но это хоть что-то. Но это обходной путь.

Вы можете разделить свои интерфейсы на два и переписать свой класс, например

interface StaticInterface {
  new(...args) => MyClass;
  fromJson(json): MyClass;
}

interface InstanceInterface {
  toJson(): string;
}

const MyClass: StaticInterface = class implements InstanceInterface {
   ...
}

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

Но тогда как насчет предложения 2? С этим ничего нельзя поделать, не так ли? Думаю, это тоже заслуживает внимания!

Какая практическая польза от одного из этих типов — как можно использовать один из них?

interface JsonSerializable {
    toJSON(): string;
    static fromJSON(serializedValue: string): JsonSerializable;
}

Уже можно сказать, что значение должно быть объектом вроде { fromJSON(serializedValue: string): JsonSerializable; } , так нужно ли это только для того, чтобы применить шаблон? Я не вижу в этом пользы с точки зрения проверки типов. В качестве примечания: в этом случае будет применяться шаблон, с которым трудно работать — было бы лучше переместить процесс сериализации в отдельные классы или функции сериализатора по многим причинам, которые я не буду здесь рассматривать.

Кроме того, почему что-то подобное делается?

class FirstChildClass extends AbstractParentClass {
    public static getSomeClassDependentValue(): string {
        return 'Some class-dependent value of class FirstChildClass';
    }
}

Как насчет использования шаблонного метода или шаблона стратегии? Это будет работать и будет более гибким, верно?

На данный момент я против этой функции, потому что мне кажется, что она добавляет ненужную сложность для описания проектов классов, с которыми трудно работать. Может быть, есть какая-то польза, которую я упускаю?

есть один допустимый вариант использования статических методов React, вот и все

@aleksey-bykov а, хорошо. В этих случаях может быть лучше, если добавление типа в свойство constructor вызовет проверку типов в этом редком сценарии. Например:

interface Component {
    constructor: ComponentConstructor;
}

interface ComponentConstructor {
    displayName?: string;
}

class MyComponent implements Component {
    static displayName = 5; // error
}

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


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

у меня была похожая идея: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

Является ли шаблон сериализации/десериализации, описанный выше, недействительным?

@dsherret «не видит преимуществ с точки зрения проверки типов». Весь смысл статического анализа в первую очередь заключается в том, чтобы выявлять ошибки как можно раньше. Я печатаю вещи так, что если сигнатуру вызова необходимо изменить, то все, кто ее вызывает, или, что особенно важно, все, кто отвечает за реализацию методов, использующих сигнатуру, обновляются до новой сигнатуры.

Предположим, у меня есть библиотека, предоставляющая набор одноуровневых классов со статическим методом foo(x: number, y: boolean, z: string) , и ожидается, что пользователи напишут фабричный класс, который берет несколько таких классов и вызывает метод для создания экземпляров. (Может быть, это deserialize или clone или unpack или loadFromServer , не имеет значения.) Пользователь также создает подклассы одного и того же (возможно, абстрактного) родителя. класс из библиотеки.

Теперь мне нужно изменить этот контракт, чтобы последним параметром был объект опций. Передача строкового значения для третьего аргумента является неисправимой ошибкой, которую следует помечать во время компиляции. Любой фабричный класс должен быть обновлен для передачи объекта при вызове foo , а подклассы, реализующие foo , должны изменить свою сигнатуру вызова.

Я хочу гарантировать, что пользователи, которые обновятся до новой версии библиотеки, поймут критические изменения во время компиляции. Библиотека должна экспортировать один из вышеперечисленных обходных путей статического интерфейса (например, type MyClass = (new (data: number[]) => MyInterface) & MyStaticInterface; ) и надеяться, что потребитель применит его во всех нужных местах. Если они забыли декорировать одну из своих реализаций или если они не использовали экспортированный из библиотеки тип для описания сигнатуры вызова foo в своем фабричном классе, компилятор не сможет сказать, что что-то изменилось, и они получат ошибки времени выполнения. . Сравните это с разумной реализацией методов abstract static в родительском классе — никаких специальных аннотаций не требуется, никакой нагрузки на потребляющий код, он просто работает из коробки.

@ thw0rted , разве большинство библиотек не потребуют какой-либо регистрации этих классов, и в этот момент может произойти проверка типов? Например:

// in the library code...
class SomeLibraryContext {
    register(classCtor: Function & { deserialize(serializedString: string): Component; }) {
        // etc...
    }
}

// then in the user's code
class MyComponent extends Comonent {
    static deserialize(serializedString: string) {
        return JSON.parse(serializedString) as Component;
    }
}

const context = new SomeLibraryContext();
// type checking occurs here today. This ensures `MyComponent` has a static deserialize method
context.register(MyComponent);

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

В качестве примечания, как разработчик интерфейса, я был бы очень раздражен, если бы меня заставили писать статические методы, потому что очень сложно внедрить зависимости в статический метод из-за отсутствия конструктора (зависимости ctor невозможны). Это также затрудняет замену реализации десериализации чем-то другим в зависимости от ситуации. Например, скажем, я десериализовал из разных источников с разными механизмами сериализации... теперь у меня есть метод статической десериализации, который по замыслу может быть реализован только одним способом для реализации класса. Чтобы обойти это, мне нужно было бы иметь другое глобальное статическое свойство или метод в классе, чтобы сообщить методу статической десериализации, что использовать. Я бы предпочел отдельный интерфейс Serializer , который позволял бы легко заменять то, что нужно использовать, и не связывал бы (де)сериализацию с (де)сериализуемым классом.

Я понимаю, что вы имеете в виду - чтобы использовать фабричный шаблон, в конечном итоге вам нужно передать реализующий класс фабрике, и в это время происходит статическая проверка. Я все еще думаю, что могут быть случаи, когда вы хотите предоставить класс, совместимый с фабрикой, но на самом деле не используете его в данный момент, и в этом случае ограничение abstract static выявляет проблемы раньше и делает смысл более ясным.

У меня есть еще один пример, который, как мне кажется, не противоречит вашему «примечанию». У меня есть несколько родственных классов во внешнем проекте, где я даю пользователю выбор, какого «поставщика» использовать для определенной функции. Конечно, я мог бы сделать словарь где-нибудь из name => Provider и использовать ключи, чтобы определить, что показывать в списке выбора, но способ, которым я это реализовал сейчас, требует статического поля name для каждой реализации провайдера.

В какой-то момент я изменил это на требование как shortName , так и longName (для отображения в разных контекстах с разным объемом доступного пространства на экране). Было бы намного проще изменить abstract static name: string; на abstract static shortName: string; и т. д. вместо того, чтобы менять компонент списка выбора на providerList: Type<Provider> & { shortName: string } & { longName: string } . Он передает намерение в нужном месте (то есть в абстрактном родительском элементе, а не в потребляющем компоненте), и его легко читать и легко изменять. Я думаю, мы можем сказать, что есть обходной путь, но я все же считаю, что он объективно хуже, чем предлагаемые изменения.

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

Моя текущая проблема: у меня есть частная библиотека .js, которую я хочу использовать в своем проекте TypeScript. Итак, я пошел дальше и начал писать файл .d.ts для этой библиотеки, но, поскольку библиотека использует статические методы, я не смог закончить это. Каков предлагаемый подход в этом случае?

Спасибо за ответы.

@greeny вот рабочее решение: https://github.com/Microsoft/TypeScript/issues/14600#issuecomment -437071092

конкретно эта его часть:

type MyClass = (new (text: string) => MyInterface) & { myStaticMethod(): string; }

Другой вариант использования: прокладки сгенерированного кода/частичного класса.

  • Я использую библиотеку @rsuter/nswag, которая генерирует спецификации чванства.
  • Вы можете написать файл «расширений», который будет объединен с сгенерированным файлом.
  • Файл расширений никогда не запускается, но он должен компилироваться сам по себе!
  • Иногда в этом файле мне нужно сослаться на что-то, чего еще не существует (потому что оно сгенерировано)
  • Поэтому я хочу объявить для него прокладку/интерфейс следующим образом
  • Примечание. Вы можете указать, что определенные операторы import игнорируются при окончательном слиянии, поэтому включение «прокладки» игнорируется в окончательном сгенерированном коде.
interface SwaggerException
{
    static isSwaggerException(obj: any): obj is SwaggerException;
}

Но я не могу этого сделать и должен написать класс - это нормально, но все же кажется неправильным. Я просто хочу, как говорили многие другие, сказать: «Это контракт».

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

Также мне бы очень хотелось видеть более чистые файлы d.ts — должно быть какое-то совпадение с этой функцией и этими файлами. Трудно понять что-то вроде JQueryStatic, потому что кажется, что это просто хак. Кроме того, реальность такова, что файлы d.ts часто устаревают и не поддерживаются, и вам нужно объявлять прокладки самостоятельно.

(извините за упоминание jQuery)

Для случая сериализации я сделал что-то подобное.

export abstract class World {

    protected constructor(json?: object) {
        if (json) {
            this.parseJson(json);
        }
    }

    /**
     * Apply data from a plain object to world. For example from a network request.
     * <strong i="6">@param</strong> json Parsed json object
     */
    abstract parseJson(json: object): void;

    /**
     * Parse instance to plane object. For example to send it through network.
     */
    abstract toJson(): object;
}

Но было бы еще проще использовать что-то вроде этого:

export abstract class World {

    /**
     * Create a world from plain object. For example from a network request.
     * <strong i="10">@param</strong> json Parsed json object
     */
    abstract static fromJson(json: object): World;

    /**
     * Parse instance to plane object. For example to send it through network.
     */
    abstract toJson(): object;
}

Я прочитал много этой темы, и я до сих пор не понимаю, почему это хорошо и правильно сказать «нет» этому шаблону. Если вы разделяете это мнение, вы также говорите, что Java и другие популярные языки делают его неправильным. Или я ошибаюсь?

Этот выпуск открыт два года и имеет 79 комментариев. Он помечен как Awaiting More Feedback . Не могли бы вы сказать, какая еще обратная связь вам нужна для принятия решения?

очень раздражает, что я не могу описать статический метод ни в интерфейсе, ни в абстрактном классе (только декларация). Так некрасиво писать обходные пути из-за того, что эта фича еще не реализована =(

Например, next.js использует статическую функцию getInitialProps для получения свойств страницы перед ее созданием. В случае выдачи страница не создается, а создается страница с ошибкой.

https://github.com/zeit/next.js/blob/master/packages/next/README.md#fetching-data-and-component-lifecycle

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

Я думаю, что эта проблема существует так долго, потому что сам JavaScript не хорош в статике 🤔

Статическое наследование никогда не должно существовать в первую очередь. 🤔🤔

Кто хочет вызвать статический метод или прочитать статическое поле? 🤔🤔🤔

  • дочерний класс: он не должен быть статическим
  • родительский класс: используйте аргумент конструктора
  • другое: используйте интерфейс ISomeClassConstructor

Есть ли другой вариант использования?🤔🤔🤔🤔

Есть какие-нибудь обновления по этому поводу?

Если это поможет, то, что я сделал в случаях, когда мне нужно ввести статический интерфейс для класса, - это использовать декоратор для принудительного применения статических членов в классе.

Декоратор определяется как:

export const statics = <T extends new (...args: Array<unknown>) => void>(): ((c: T) => void) => (_ctor: T): void => {};

Если у меня есть статический интерфейс члена конструктора, определенный как

interface MyStaticType {
  new (urn: string): MyAbstractClass;
  isMember: boolean;
}

и вызывается в классе, который должен статически объявить членов T как:

@statics<MyStaticType>()
class MyClassWithStaticMembers extends MyAbstractClass {
  static isMember: boolean = true;
  // ...
}

Самый частый пример хорош:

interface JsonSerializable {
    toJSON(): string;
    static fromJSON(serializedValue: string): JsonSerializable;
}

Но как сказано в #13462 здесь :

Интерфейсы должны определять функциональные возможности, предоставляемые объектом. Эта функциональность должна быть переопределяемой и взаимозаменяемой (поэтому методы интерфейса являются виртуальными). Статика — это концепция, параллельная динамическому поведению/виртуальным методам.

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

Итак, я могу предложить следующее, со всеми его недостатками:


Интерфейс может быть описанием объекта или класса . Допустим, интерфейс класса отмечен ключевыми словами class_interface .

class_interface ISerDes {
    serialize(): string;
    static deserialize(str: string): ISerDes
}

Классы (и интерфейсы классов) могут использовать ключевые слова statically implements для объявления своих статических символов с использованием объектного интерфейса (интерфейсы классов не могут быть реализованы statically ).

Классы (и интерфейсы классов) по-прежнему будут использовать ключевое слово implements с интерфейсом объекта или интерфейсом класса .

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

interface ISerializable{
    serialize(): string;
}
interface IDeserializable{
    deserialize(str: string): ISerializable
}

class_interface ISerDes implements ISerializable statically implements IDeserializable {}

Таким образом, интерфейсы могли бы сохранить свое значение, а class_interface стал бы новым видом абстракционного символа, предназначенного для определений классов.

Еще один небольшой некритичный вариант использования:
В Angular для компиляции AOT вы не можете вызывать функции в декораторах (как в декораторе модуля @NgModule )
угловая проблема

Для модуля сервисного работника вам нужно что-то вроде этого:
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
Наша среда использует подклассы, расширяя абстрактный класс со значениями по умолчанию и абстрактными свойствами для реализации. Аналогичный пример реализации
Таким образом, AOT не работает, потому что создание класса является функцией и выдает ошибку, например: Function calls are not supported in decorators but ..

Чтобы заставить его работать и поддерживать автодополнение/компилятор, можно определить те же свойства на статическом уровне. Но для «реализации» свойств нам нужен интерфейс со статическими членами или абстрактными статическими членами в абстрактном классе. Оба пока невозможны.

с интерфейсом может работать так:

// default.env.ts
interface ImplementThis {
  static propToImplement: boolean;
}

class DefaultEnv {
  public static production: boolean = false;
}

// my.env.ts
class Env extends DefaultEnv implements ImplementThis {
  public static propToImplement: true;
}

export const environment = Env;

с абстрактной статикой может работать так:

// default.env.ts
export abstract class AbstractDefaultEnv {
  public static production: boolean = false;
  public abstract static propToImplement: boolean;
}
// my.env.ts
class Env extends AbstractDefaultEnv {
  public static propToImplement: true;
}

export const environment = Env;

есть обходные пути , но все они слабые :/

@DanielRosenwasser @RyanCavanaugh извиняюсь за упоминания, но похоже, что это предложение функции, которое получило большую поддержку сообщества, и я чувствую, что его было бы довольно легко реализовать, глубоко похоронено в категории «Проблемы». Есть ли у кого-нибудь из вас какие-либо комментарии об этой функции, и будет ли приветствоваться PR?

Разве эта проблема не является дубликатом # 1263? 😛

26398 (статические элементы проверки типа на основе свойства конструктора типа реализации) выглядит как лучшее решение... если что-то подобное должно быть реализовано, то я надеюсь, что это именно оно. Это не требует дополнительного синтаксиса/анализа и представляет собой изменение проверки типа только для одного сценария. Это также не вызывает столько вопросов, как это.

Я чувствую, что статические методы в интерфейсах не так интуитивно понятны, как наличие статических абстрактных методов в абстрактных классах.

Я думаю, что добавление статических методов к интерфейсам немного поверхностно, потому что интерфейс должен определять объект, а не класс. С другой стороны, абстрактному классу, безусловно, должно быть разрешено иметь статические абстрактные методы, поскольку абстрактные классы используются для определения подклассов. Что касается реализации этого, то просто нужно будет проверять тип при расширении абстрактного класса (например, class extends MyAbstractClass ), а не при использовании его в качестве типа (например, let myInstance: MyAbstractClass ).

Пример:

abstract class MyAbstractClass {
  static abstract bar(): number;
}

class Foo extends MyAbstractClass {
  static bar() {
    return 42;
  }
}

прямо сейчас по необходимости я использую это

abstract class MultiWalletInterface {

  static getInstance() {} // can't write a return type MultiWalletInterface

  static deleteInstance() {}

  /**
   * Returns new random  12 words mnemonic seed phrase
   */
  static generateMnemonic(): string {
    return generateMnemonic();
  }
}

это неудобно!

Я пришел с проблемой, когда я добавляю свойства к «Объекту», вот пример песочницы

interface Object {
    getInstanceId: (object: any) => number;
}

Object.getInstanceId = () => 42;
const someObject = {};
Object.getInstanceId(someObject); // correct
someObject.getInstanceId({}); // should raise an error but does not

Любой экземпляр объекта теперь считается имеющим свойство getInstanceId , в то время как только Object должен. Со статическим свойством проблема была бы решена.

Вы хотите расширить ObjectConstructor, а не Object. Вы объявляете метод экземпляра, когда на самом деле хотите присоединить метод к самому конструктору. Я думаю, что это уже возможно через слияние объявлений:

````тс
объявить глобальный {
интерфейс ObjectConstructor {
привет(): строка;
}
}

Объект.привет();
````

@thw0rted Отлично! спасибо, я не знал об ObjectConstructor

Более важный момент заключается в том, что вы увеличиваете тип конструктора, а не тип экземпляра. Я только что просмотрел объявление Object в lib.es5.d.ts и обнаружил, что оно имеет тип ObjectConstructor .

Мне трудно поверить, что эта проблема все еще актуальна. Это действительно полезная функция с несколькими реальными вариантами использования.

Весь смысл TypeScript в том, чтобы обеспечить безопасность типов в нашей кодовой базе, так почему же эта функция все еще «ожидает отзывов» после двух лет отзывов?

Я могу ошибаться в этом, но сможет ли что-то вроде метаклассов Python решить эту проблему нативным, санкционированным способом (т. е. не хаком или обходным путем), не нарушая парадигму TypeScript (т. е. сохраняя отдельные типы TypeScript для экземпляра и класс)?

Что-то вроде этого:

interface DeserializingClass<T> {
    fromJson(serializedValue: string): T;
}

interface Serializable {
    toJson(): string;
}

class Foo implements Serializable metaclass DeserializingClass<Foo> {
    static fromJson(serializedValue: string): Foo {
        // ...
    }

    toJson(): string {
        // ...
    }
}

// And an example of how this might be used:
function saveObject(Serializable obj): void {
    const serialized: string = obj.toJson();
    writeToDatabase(serialized);
}

function retrieveObject<T metaclass DeserializingClass<T>>(): T {
    const serialized: string = getFromDatabase();
    return T.fromJson(serialized);
}

const foo: Foo = new Foo();
saveObject(foo);

const bar: Foo = retrieveObject<Foo>();

Честно говоря, кажется, что самая сложная часть этого подхода заключается в том, чтобы придумать осмысленное ключевое слово в стиле TypeScript для metaclass ... staticimplements , classimplements , withstatic , implementsstatic ... не уверен.

Это немного похоже на предложение @GerkinDev , но без отдельного интерфейса. Здесь существует единая концепция интерфейса, и их можно использовать для описания формы экземпляра или класса. Ключевые слова в определении реализующего класса указывают компилятору, с какой стороны следует проверять каждый интерфейс.

Давайте продолжим обсуждение в #34516 и #33892 в зависимости от того, какую функцию вы собираетесь использовать.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги