Typescript: Предложение: разрешить доступ get/set к разным типам

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

Было бы здорово, если бы был способ ослабить текущее ограничение, требующее, чтобы методы доступа get/set имели один и тот же тип. это было бы полезно в такой ситуации:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

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

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: moment.Moment) {
        assert.fail('Setter for myDate is not available. Please use: setMyDate() instead');
    }

    setMyDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

Это далеко от идеала, и код был бы намного чище, если бы были разрешены разные типы.

Спасибо!

Design Limitation Suggestion Too Complex

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

Геттер и сеттер JavaScript с различными типами вполне допустимы и отлично работают, и я считаю, что это главное преимущество/цель этой функции. Необходимость предоставлять setMyDate() только для того, чтобы угодить TypeScript, разрушает его.

Подумайте также о чистых JS-библиотеках, которые будут следовать этому шаблону: .d.ts должен будет предоставлять объединение или any .

Проблема в том, что средства доступа не отображаются в .d.ts иначе, чем обычные свойства.

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

// MyClass.d.ts

// Instead of generating:
declare class MyClass {
  myDate: moment.Moment;
}

// Should generate:
declare class MyClass {
  get myDate(): moment.Moment;
  set myDate(value: Date | moment.Moment);
}

// Or shorter syntax:
declare class MyClass {
  myDate: (get: moment.Moment, set: Date | moment.Moment);
  // and 'fooBar: string' being a shorthand for 'fooBar: (get: string, set: string)'
}

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

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

Спасибо за быстрый ответ, Дэн. Я пойду менее элегантным путем. Спасибо за отличную работу!

Геттер и сеттер JavaScript с различными типами вполне допустимы и отлично работают, и я считаю, что это главное преимущество/цель этой функции. Необходимость предоставлять setMyDate() только для того, чтобы угодить TypeScript, разрушает его.

Подумайте также о чистых JS-библиотеках, которые будут следовать этому шаблону: .d.ts должен будет предоставлять объединение или any .

Проблема в том, что средства доступа не отображаются в .d.ts иначе, чем обычные свойства.

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

// MyClass.d.ts

// Instead of generating:
declare class MyClass {
  myDate: moment.Moment;
}

// Should generate:
declare class MyClass {
  get myDate(): moment.Moment;
  set myDate(value: Date | moment.Moment);
}

// Or shorter syntax:
declare class MyClass {
  myDate: (get: moment.Moment, set: Date | moment.Moment);
  // and 'fooBar: string' being a shorthand for 'fooBar: (get: string, set: string)'
}

Я понимаю, что это всего лишь мнение, но написание сеттера таким образом, что a.x === y не равно true сразу после a.x = y; , является гигантским красным флагом. Как потребители такой библиотеки узнают, какие свойства имеют магические побочные эффекты, а какие нет?

Откуда [вы] знаете, какие свойства имеют магические побочные эффекты, а какие нет?

В JavaScript это может быть неинтуитивно, в TypeScript инструменты (IDE + компилятор) будут жаловаться. Почему мы снова любим TypeScript? :)

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

Это доказывает, что JavaScript слабо типизирован, поэтому TypeScript должен быть слабо типизирован. :-С

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

C# это позволяет, и это не делает этот язык слабо типизированным . C# не позволяет получать/устанавливать разные типы.

C# это позволяет, и это не делает этот язык слабо типизированным . C# не позволяет получать/устанавливать разные типы.

:подмигивание:

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

Часто требуется скопировать простой старый объект в экземпляры объекта.

    get fields(): Field[] {
      return this._fields;
    }

    set fields(value: any[]) {
      this._fields = value.map(Field.fromJson);
    }

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

@paulwalker шаблон, который вы там используете (сеттер, принимающий any , и геттер, возвращающий более конкретный тип), действителен сегодня.

@danquirk Приятно знать, спасибо! Похоже, мне просто нужно было обновить компилятор плагинов IDE для ST.

@danquirk Похоже , это не работает в соответствии с игровой площадкой (или версией 1.6.2):
http://www.typescriptlang.org/Playground#src =%0A%0Aclass%20Foo%20%7B%0A%0A%20%20get%20items()%3A%20string%5B%5D%20%7B%0A %09%20%20return%20%5B%5D%3B%0A%20%20%7D%0A%20%20%0A%20%20set%20items(значение%3A%20любое)%20%7B%0A% 09%20%20%0А%20%20%7Д%0А%7Д

Я только что протестировал с typescript@next (версия 1.8.0-dev.20151102) и тоже получил ошибку.

~$ tsc --version
message TS6029: Version 1.8.0-dev.20151102
~$ cat a.ts
class A {
    get something(): number {return 5;}
    set something(x: any) {}
}

~$ tsc -t es5 a.ts
a.ts(2,2): error TS2380: 'get' and 'set' accessor must have the same type.
a.ts(3,2): error TS2380: 'get' and 'set' accessor must have the same type.

По иронии судьбы, после обновления моего линтера Sublime он больше не выдавал ошибку, связанную с использованием TypeScript 1.7.x. Я предполагал, что это предстоящее улучшение в 1.7+, поэтому, возможно, 1.8.0 регрессировал.

Даже с версией кода Visual Studio (0.10.5 (декабрь 2015 г.)), которая поддерживает машинописный текст 1.7.5 и с глобально установленным машинописным текстом 1.7.5 на моей машине, это все еще проблема:

image

Так какая версия будет поддерживаться?
Спасибо

Я думаю, Дэн ошибся. Геттер и сеттер должны быть одного типа.

стыд. была бы хорошей функцией при написании объектов страницы для использования в тестах транспортира.

Я мог бы написать тест транспортира:

po.email = "[email protected]";
expect(po.email).toBe("[email protected]");

... создав объект страницы:

class PageObject {
    get email(): webdriver.promise.Promise<string> {
        return element(by.model("ctrl.user.email")).getAttribute("value")
    }
    set email(value: any) {
        element(by.model("ctrl.user.email")).clear().sendKeys(value);
    }
}

Как насчет того кода, который требует, чтобы установщик имел тип any ?

Геттер вернет webdriver.promise.Promise<string> , а сеттеру я хочу присвоить значение string .

Возможно, следующая более длинная форма теста транспортира делает его более понятным:

po.email = "[email protected]";
var currentEmail : webdriver.promise.Promise<string> = po.email;
expect(currentEmail).toBe("[email protected]")

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

class Style {
    private _width: number = 5;

    // `: number | null` encumbers callers with unnecessary `!`
    get width(): number {
        return this._width;
    }

    // `: number` prevents callers from passing in null
    set width(newWidth: number | null) {
        if (newWidth === null) {
            this._width = 5;
        }
        else {
            this._width = newWidth;
        }
    }
}

Не могли бы вы хотя бы позволить типам различаться наличием | null и | undefined ?

Это было бы действительно хорошей функцией.

Так будет ли это особенностью?

@artyil закрыт и помечен _By Design_, что указывает на то, что в настоящее время нет никаких планов по его добавлению. Если у вас есть убедительный вариант использования, который, по вашему мнению, перевешивает высказанные выше опасения, вы можете свободно излагать свои доводы, а дополнительные отзывы могут заставить основную команду пересмотреть свою позицию.

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

Я согласен.

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

Хотя я люблю TypeScript и ценю все усилия, вложенные в него командой (правда, ребята, молодцы!), я должен признать, что разочарован этим решением. Это было бы гораздо лучшим решением, чем альтернативы getFoo()/setFoo() или get foo()/set foo()/setFooEx() .

Краткий список проблем:

  • В настоящее время мы предполагаем, что свойства имеют ровно один тип. Теперь нам нужно различать тип «чтения» и тип «записи» каждого свойства в каждом месте.
  • Все отношения типов становятся существенно более сложными, потому что мы должны рассуждать о двух типах для каждого свойства вместо одного (может ли { get foo(): string | number; set foo(): boolean } присваиваться { foo: boolean | string | number } или наоборот?)
  • В настоящее время мы предполагаем, что свойство после установки все еще имеет тип установленного значения в следующей строке (что, по-видимому, является неправильным предположением в кодовых базах некоторых людей, а?). Вероятно, нам просто нужно «отключить» любой анализ управления потоком для таких свойств.

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

foo.bar = "hello";
console.log(foo.bar);

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

@RyanCavanaugh , хотя я согласен с вами по поводу введенного мнения, я вижу один встречный аргумент, который _может_ быть просто очень TypeScript ... Бросать что-то слабо типизированное в сеттер, но всегда возвращая что-то строго типизированное, например:

foo.bar = [ '1', 2 ];  // any[]
console.log(foo.bar);  // number[]: [ 1, 2 ]

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

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

Например, свойство цвета может быть выражено как экземпляр Color или как строка CSS, такая как rgba(r, g, b, a) , или как массив из 3 или 4 чисел. Свойство по-прежнему имеет тип экземпляра Color , так как это тип того, что вы получаете при чтении значения.

Некоторая информация об этом: https://developers.arcgis.com/javascript/latest/guide/autocasting/index.html .

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

Другой пример для этой проблемы: https://github.com/gulpjs/vinyl#filebase

file.base = 'd:\\dev';
console.log(file.base); //  'd:\\dev'
file.base = null;
console.log(file.base); //  'd:\\dev\\vinyl' (returns file.cwd)

Таким образом, сеттер — string | null | undefined , а геттер — string . Какой тип мы должны использовать в определениях типов для этой библиотеки? Если мы воспользуемся первым, компилятор потребует везде бесполезных проверок на null. Если мы воспользуемся вторым, мы не сможем присвоить этому свойству null .

У меня есть еще один пример, в котором я хотел бы, чтобы геттер возвращал значение nullable, но где сеттер никогда не должен допускать значение null в качестве ввода, стилизованное под:

class Memory {
    public location: string;
    public time: Date;
    public company: Person[];
}

class Person
{
    private _bestMemoryEver: Memory | null;

    public get bestMemoryEver(): Memory | null { // Might not have one yet
        return this._bestMemoryEver;
    }

    public set bestMemoryEver(memory: Memory) { // But when he/she gets one, it can only be replaced, not removed
        this._bestMemoryEver = memory;
    }
}

var someDude = new Person();
// ...
var bestMemory: Memory | null = someDude.bestMemoryEver;
//...
someDude.bestMemoryEver = null; // Oh no you don't!

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

@Elephant-Vessel С философской точки зрения мне очень нравится этот пример, он хорошо представляет непостоянную природу людей, но я не уверен, что он не будет представлять это еще лучше, если позволит null (или undefined ) быть установленным. Как я могу смоделировать отказ синапса в системе?

@aluanhaddad Почему вам нужен сбой синапса? Я не хочу таких плохих вещей в своей вселенной ;)

Есть новости по этому поводу? Как насчет значения по умолчанию, когда установлено значение null или undefined?

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

Ниже то, что я хотел бы иметь:

export class TestClass {
  private _prop?: number;

  get prop(): number {
    // return default value if not defined
    this._prop === undefined ? 0 : this._prop;
  }
  set prop(val: number | undefined) {
    this._prop = val;
  }
}

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

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

@mhegazy @kitsonk Я могу быть предвзятым, но я чувствую, что это ошибка , которая возникла для строгой проверки нуля по общему шаблону, особенно в других языках, таких как фигурные скобки, где у них еще нет проверки нуля. Обходной путь потребует, чтобы потребитель использовал оператор взрыва или проверил, что он на самом деле никогда не равен нулю (в этом смысл никогда не быть нулевым при использовании значений по умолчанию).

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

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

В ответ на https://github.com/Microsoft/TypeScript/issues/2521#issuecomment -199650959
Вот дизайн предложения, который должен быть менее сложным для реализации:

export interface Test {
  undefset prop1: number; // property [get] type number and [set] type number | undefined
  nullset prop2: number; // property [get] type number and [set] type number | null
  nilset prop3: number; // property [get] type number and [set] type number | null | undefined
  undefget prop4: number; // property [get] type number | undefined and [set] type number
  nullget prop5: number; // property [get] type number | null and [set] type number
  nilget prop6: number; // property [get] type number | null | undefined and [set] type number
}

Похоже, что есть люди, наблюдающие за этой веткой, которые гораздо лучше знакомы с TypeScript, поэтому, возможно, кто-то, кто все еще обращает внимание, может ответить на связанный вопрос. В этой проблеме с Cesium я упомянул ограничение типа get/set, которое мы здесь обсуждаем, и ребята из Cesium сказали, что шаблон, которому они следуют, пришел из C# — это неявный конструктор .

Может ли TypeScript поддерживать неявные конструкторы? То есть, могу ли я сказать, что myThing.foo всегда возвращает Bar и может быть напрямую назначено Bar , но также может быть назначено number , которое будет быть незаметно завернутым в / использованным для инициализации Bar для удобства разработчика? Если это возможно сделать, аннотировав Bar или, может быть, специально указав, что « number можно присвоить Bar<number> », это будет относиться к варианту использования, обсуждаемому в проблеме цезия, а также многие вопросы, затронутые в этой теме.

Если нет, нужно ли предлагать неявную поддержку конструктора в отдельной проблеме?

Чем больше я думаю/читаю об этом, тем больше я уверен, что «шаблону неявного конструктора» понадобится функция, описанная в этом выпуске. Единственный способ, которым это возможно в vanilla JS, - это использование методов доступа к объекту get/set, потому что это единственный раз, когда номинальное назначение (оператор = ) фактически вызывает определяемую пользователем функцию. (Правильно?) Итак, я думаю, нам действительно понадобится

class MyThing{
  set foo(b: Bar<boolean> | boolean);
  get foo(): Bar<boolean>;
}

что, похоже , @RyanCavanaugh считает не «нормальной семантикой».

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

JavaScript уже допускает описанный вами шаблон. Сложность заключается в том, что стороны чтения и записи типов в TypeScript по замыслу предполагаются одинаковыми. В язык была внесена модификация для запрета присваивания ( readonly ), но есть несколько проблем, которые потребовали концепции _write only_, которая обсуждалась как слишком сложная для небольшой реальной ценности.

IMO, с тех пор, как JavaScript разрешил аксессоры, люди потенциально создали с ними запутанные API. Меня лично сбивает с толку то, что что-то при задании _магическим_ образом_ меняется на что-то другое. Неявное что-либо, особенно преобразование типов, является бичом JavaScript IMO. Именно гибкость вызывает проблемы. С этими типами преобразований, где есть _magic_, мне лично нравится видеть вызываемые методы, где для потребителя немного более явно, что произойдет какой-то вид ✨ и сделает геттер только для чтения для извлечения значений.

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

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

Хорошо это или плохо, но JS дал нам возможность превращать присваивания в вызовы функций, и люди этим пользуются. Без возможности назначать разные типы парам set/get , зачем вообще иметь set/get во внешней типизации? Salsa не нужно знать, что свойство реализовано с помощью геттера и сеттера, если оно всегда будет обрабатывать myThing.foo как переменную-член одного типа, независимо от того, на какой стороне присваивания оно находится. (Очевидно, что фактическая компиляция TypeScript — это совсем другое.)

@thw0rted

Похоже, что есть люди, наблюдающие за этой веткой, которые гораздо лучше знакомы с TypeScript, поэтому, возможно, кто-то, кто все еще обращает внимание, может ответить на связанный вопрос. В этой проблеме Cesium я упомянул ограничение типа get/set, которое мы здесь обсуждаем, и ребята из Cesium сказали, что шаблон, которому они следуют, исходит из C# — это неявный конструктор.

Неявные определяемые пользователем операторы преобразования C# выполняют генерацию статического кода на основе типов значений. Типы TypeScript стираются и не влияют на поведение во время выполнения (не считая крайних случаев async / await для полифиллеров Promise ).

@kitsonk В целом я не согласен с свойствами. Проведя довольно много времени с Java, C++ и C#, я очень люблю свойства (как видно из C#), потому что они обеспечивают критическую синтаксическую абстракцию (это отчасти верно в JavaScript). Они позволяют интерфейсам осмысленно разделять возможности чтения/записи без изменения синтаксиса. Я ненавижу смотреть, как глаголы тратятся впустую на тривиальные операции, такие как getX() , когда получение X может быть неявным.
IMO запутанные API, многие из которых, как вы говорите, злоупотребляют средствами доступа, больше связаны с тем, что слишком много _setters_ делают магические вещи.

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

interface Entry {key: string; value: any;}

export function createRegistry() {
  let entries: Entry[] = [];
  return {
    register(key: string, value: any) {
      entries = [...entries, {key, value}];
    },
    get entries() {
      return [...entries];
    }
  }
}

const registry = createRegistry();

registry.register('hello', '您好');
console.log(registry.entries); //[{key: 'hello', value: '您好'}]
registry.register('goodbye', '再见');
console.log(registry.entries); //[{key: 'hello', value: '您好'}, {key: 'goodbye', value: '再见'}]

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

Когда TypeScript ограничивает JavaScript, это становится скорее неприятностью, чем преимуществом. Разве TypeScript не предназначен для того, чтобы помогать разработчикам общаться друг с другом?

Кроме того, сеттеры не зря называются мутаторами. Если бы мне не нужно было никакого преобразования, я бы не использовал сеттер, я бы установил переменную сам.

перенос проекта javascript на typescript. я встретил эту проблему ..

Это было бы неплохо использовать в угловых декораторах @Input . Поскольку значение передается из шаблона, на мой взгляд, было бы намного чище работать с различными типами входящих объектов.

Обновление: похоже, это работает для меня

import { Component, Input } from '@angular/core';
import { flatMap, isString, isArray, isFalsy } from 'lodash';

@Component({
  selector: 'app-error-notification',
  templateUrl: './error-notification.component.html',
})

export class ErrorNotificationComponent {
  private _errors: Array<string> = [];
  constructor() { }
  /**
   * 'errors' is expected to be an input of either a string or an array of strings
   */
  @Input() set errors(errors: Array<string> | any){
      // Caller just passed in a string instead of an array of strings
      if (isString(errors)) {
        this._errors = [errors];
      }
      // Caller passed in array, assuming it is a string array
      if (isArray(errors)) {
        this._errors = errors;
      }
      // Caller passed in something falsy, which means we should clear error list
      if (isFalsy(errors)) {
        this._errors = [];
      }
      // At this point just set it to whatever might have been passed in and let
      // the user debug when it is broken.
      this._errors = errors;
  }

  get errors() {
    return this._errors;
  }
}

Так были ли мы? Мне бы очень хотелось иметь возможность возвращать с помощью геттера другой тип, чем из сеттера. Например:

class Field {
  private _value: string;

  get value(): string {
    return this._value;
  }

  set value(value: any) {
    this._value = String(value);
  }
}

Это то, что делает 99% нативных реализаций (если вы передаете число (input as HTMLInputElement).value , оно всегда будет возвращать строку. На самом деле get и set следует рассматривать как методы и должны разрешать некоторые из них:

set value(value: string);
set value(value: number);
set value(value: any) {
  this._value = String(value);
}
  // AND/OR
set value(value: string | number) {
  this._value = String(value);
}

@raysuelzer , когда вы говорите, что ваш код «работает», разве ErrorNotificationComponent.errors не возвращает тип Array<string> | any ? Это означает, что вам нужны охранники типов каждый раз, когда вы их используете, когда вы знаете, что на самом деле они могут вернуть только Array<string> .

@lifaon74 , насколько я знаю, по этому вопросу нет никакого движения. Я думаю, что был сделан убедительный довод — представлено несколько сценариев, тонны устаревшего кода JS, который из-за этого не может быть должным образом описан в Typescript — но проблема закрыта. Может быть, если вы думаете, что факты на местах изменились, открыть новый? Я не знаю политики команды в отношении повторения старых аргументов, но я бы вас поддержал.

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

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

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

Лично я считаю, что вопрос решен, если мы придумаем какой-нибудь не совсем обременительный обходной путь для устаревшей проблемы с JS. Я все еще новичок в ТС, так что, может быть, мое текущее решение (принудительное литье или ненужная защита типа) не является оптимальным использованием существующих возможностей?

Согласен. setter будет настолько ограничен, когда тип должен быть одинаковым... Пожалуйста, улучшите.

Ищу рекомендации по выполнению этого:

get price() {
    return (this._price as number);
  }

  set price(price) {
    this._price = typeof price === 'string' ? parseFloat(parseFloat(price).toFixed(8)) : parseFloat(price.toFixed(8));
  }

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

Я думаю, что вердикт состоит в том, что это допустимый (или, по крайней мере, достаточно хорошо принятый) шаблон Javascript, который не очень хорошо сочетается с внутренними компонентами Typescript. Может быть, если изменение внутренних компонентов является слишком большим изменением, мы могли бы получить какую-то аннотацию, которая изменяет способ компиляции TS внутри? Что-то типа

class Widget {
  get price(): number | /** <strong i="7">@impossible</strong> */ string | undefined { return this._price; }
  set price(val: number|string|undefined){ ... }
}

let w = new Widget();
w.price = 10;
// Annotation processes as "let p:number|undefined = (w.price as number|undefined)"
let p: number|undefined = w.price;

Другими словами, возможно, мы могли бы разметить TS таким образом, чтобы препроцессор (?) преобразовывал все операции чтения w.price в явное приведение до того, как TS будет транспилирован в JS. Конечно, я не знаю внутренностей того, как TS управляет аннотациями, так что это может быть полным мусором, но общая идея заключается в том, что, возможно, было бы проще каким-то образом превратить TS в другой TS, чем изменить то, как транспилятор TS генерирует JS.

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

class Widget {
    get price(): number { return this._price; }
    set price(val){ return this.setPrice(val); } // call another function

    // do processing here
    private setPrice(price: number | string): number {
        let num = Number(price);
        return isNaN(num) ? 0 : num;
    }
}

@iamjoyce widget.price = '123' выдает ошибку компиляции в вашем коде

@iamjoyce => неправильно, потому что компилятор предполагает val: number в set price(val)

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

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

private _someProperty: SomeEnum;
set someProperty(value: SomeEnum | string) {
   this._someProperty = this.coerceSomeEnum(value);
} 
get someProperty(): SomeEnum {
  return this._someProperty;
}

Сегодня это работает, если мы опустим | string , но это будет более точно описывать, как Angular в конечном итоге использует компонент. В этом случае, если вы обращаетесь к нему из кода, тип свойства имеет значение, но если вы устанавливаете его как атрибут, он будет выполнять перебор строки.

Я не думаю, что нам обычно нужна эта функциональность, потому что мы ХОТИМ проектировать API таким образом. Я согласен, что свойства лучше, если они не делают побочных эффектов принуждения под прикрытием. Чтобы обойти это, было бы здорово, если бы Angular был разработан таким образом, чтобы наборы атрибутов и наборы связывающих свойств входили в разные точки входа.

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

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

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

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

Но, возможно, это способ решить проблему внутренне. Пока сеттер является просто объединением надмножества типа геттера. Рассматривайте типы геттера и сеттера как эквивалентные внутри, но помечайте части типа объединения геттера как невозможные. Так что анализ потока управления исключает их из рассмотрения. Будет ли это обойти ограничения дизайна?

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

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

Определяемая пользователем защита типа для возвращаемого значения также имеет то преимущество, что может быть выражена в объявлениях/интерфейсах типов при сжатии до свойства?

Просто добавим еще один вариант использования здесь, mobx-state-tree позволяет установить свойство несколькими различными способами (из типов экземпляров и снимков), однако он всегда будет возвращать его только одним стандартным способом (экземпляры) из аксессора get, так что это было бы чрезвычайно полезно, если бы оно поддерживалось.

Я обхожу это так:

interface SomeNestedString {
  foo: string;
}

...

private _foo: SomeNestedString | string;

get foo(): SomeNestedString | string {
  return this._foo;
}

set foo(value: SomeNestedString | string) {
  this._foo = (value as SomeNestedString).foo;
}

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

Я использую any для обхода TSLint. И компилятор тоже не жалуется.

export class FooBar {
  private bar: string;

  get foo (): string | any {
    return this.bar;
  }

  set foo (value: Date | string | any) {
    // Type guarding enables IntelliSense in VS Code
    if (value instanceof Date) {
      this.bar = value.toISOString();
    } else if (typeof value === 'string') {
      this.bar = value;
    } else {
      this.bar = String(value); // Or throw an error
    }
  }
}

@jpidelatorre таким образом вы теряете безопасность типов.

const fooBar = new FooBar()
const a: number = fooBar.foo // works while it should fail
fooBar.foo = 123 // fails only at runtime, not compile time. It doesnt fail in this particular case with strings and numbers, but it will with something more complex

@keenondrums Именно поэтому мы хотим, чтобы у методов доступа были разные типы. То, что я нашел, это обходной путь, а не решение.

@jpidelatorre мой текущий обходной путь - использовать другую функцию в качестве установщика

export class FooBar {
  private bar: string;

  get foo (): string {
    return this.bar;
  }

  setFoo (value: Date | string ) {}
}

@keenondrums Не так красиво выглядит, как аксессуары, но пока лучший вариант.

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

Что слишком некрасиво.

Я также хотел бы эту функцию, но мне нужно, чтобы она хорошо работала в файлах .d.ts, где нет обходных путей. Я пытаюсь задокументировать некоторые классы, открытые через Mocha (мост Objective-C/Javascript), и свойства экземпляра обернутых элементов настроены следующим образом:

class Foo {
    get bar:()=>number;
    set bar:number;
}

const foo = new Foo();
foo.bar = 3;
foo.bar(); // 3

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

interface IFoo {
    bar: string;
}

class Foo implements IFoo {
    bar: string;
    toString():string;
}

class Example {
    get foo:Foo;
    set foo:Foo|IFoo;
}

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

Изменяет ли математику тот факт, что это делает невозможным точное описание основного DOM API? @РайанКавано говорит

Я действительно возражаю против идеи, что этот код foo.bar = "hello"; console.log(foo.bar); должен когда-либо печатать что-либо кроме «привет» на языке, который пытается иметь нормальную семантику.

Мы могли бы спорить о том, следует ли использовать его таким образом, но DOM всегда поддерживал конструкции вроде el.hidden=1; console.log(el.hidden) // <-- true, not 1 . Это не просто шаблон, который используют несколько человек, он не просто находится в популярной библиотеке, поэтому может быть хорошей идеей поддерживать его. Это основной принцип того, как всегда работал JS — немного DWIM, встроенный в душу языка — и невозможность этого на уровне языка нарушает основополагающий принцип TS, что он должен быть «надмножеством» JS. . Это уродливый пузырь, торчащий из диаграммы Венна, и мы не должны об этом забывать.

Вот почему мне все еще нравится идея, чтобы сеттер/геттер имел один и тот же тип:

number | boolean

Но введение своего рода typegaurd, где вы можете указать, что, хотя геттер технически имеет тот же тип объединения, на самом деле он когда-либо будет возвращать только подмножество типов в объединении. Разве рассмотрение этого как своего рода сужающего gaurd на геттере (что-то вроде потока вывода?) не делает его более простым, чем модификация модели типов? (Он говорит, что ничего не знает о внутренностях...)

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

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

👍 !

Я думаю, что мы все можем согласиться с тем, что у вас не должно быть возможности получить тип, который не может быть установлен; Мне просто нужен какой-то способ автоматически сузить тип на get , чтобы он был типом, который, как я знаю, всегда будет.

Я не понимаю, почему некоторые возражают, что это нарушит безопасность типов и все такое. Я не вижу нарушения какой-либо безопасности типов, если мы разрешаем, по крайней мере, установщику иметь другой тип, потому что в любом случае у нас есть проверка, что вы не позволите другому типу установить свойство.
Бывший:
Скажем, у меня есть свойство как массивно из БД это будет возвращено в виде строки с разделением запятой, например, «10,20,40». Но сейчас я не могу сопоставить это со свойством режима, поэтому было бы очень полезно, если бы вы разрешили

частный _employeeIdList: номер []

получить EmployeeIDList (): число [] {
вернуть this._employeeIdList ;
}
установить EmployeeIDList (_idList: любой ) {
если (тип _idList == 'строка') {
this._employeeIdList = _idList.split(',').map(d => Number(d));
}
иначе если (typeof _idList== 'объект') {
this._employeeIdList = _idList as number[];
}
}

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

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

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

Я думаю, что TypeScript прекрасен тем, что позволяет нам быть очень выразительными при описании недостатков существующих JS API, даже если у них нет идеально структурированных API. Я думаю, что это сценарий, в котором, если бы TypeScript был достаточно выразительным, чтобы позволить нам указать, что геттер всегда будет возвращать тип, который является строгим подмножеством типа геттера, это добавило бы огромное значение в моделирование существующих API, даже ДОМ!

Trusted Types — это новое предложение API браузера для борьбы с DOM XSS. Это уже реализовано в Chromium (за флажком). Основная часть API изменяет установщики различных свойств DOM, чтобы они принимали доверенные типы. Например, .innerHTML принимает TrustedHTML | string , но всегда возвращает string . Чтобы описать API на TypeScript, нам нужно исправить эту проблему.

Отличие от предыдущих комментариев в том, что это API браузера (а не пользовательская библиотека), который нельзя легко изменить. Кроме того, влияние изменения типа Element.innerHTML на any (что является единственным возможным решением в настоящее время) больше, чем неточное описание пользовательской библиотеки.

Есть ли шанс, что этот пулл-реквест будет переоткрыт? Или есть другие решения, которые я пропустил?

Копия: @mprost , @koto.

В языке, который поддерживает объединение типов, таком как TypeScript, эта функция является естественной и привлекательной.

@RyanCavanaugh, даже если геттер и установщик имеют один и тот же тип, не гарантируется, что o.x === y после o.x = y , поскольку установщик может выполнить некоторую очистку перед сохранением значения.

element.scrollTop = -100;
element.scrollTop; // returns 0

Я поддерживаю беспокойство @vrana. Текущее поведение делает невозможным моделирование некоторых существующих API в Typescript.

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

Сужение типов геттеров позволяет Typescript представлять эти API, что сейчас невозможно.

Вы можете представлять эти API, потому что правильно сказать, что тип свойства — это объединение возможных типов, которые вы можете предоставить установщику или получить от приемника.

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

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

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

Зачем допускать это? Простота использования.

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

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

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

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

type urlRep = string | Url;

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

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

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

И я не думаю, что кто-то когда-либо захочет, чтобы установщик/получатель имел _disjoint_ типы. Здесь обсуждается то, что производитель API утверждает потребителю API, что метод получения вернет значение с типом, который является строгим подмножеством типа объединения метода установки.

И я не думаю, что кто-то когда-либо захочет, чтобы у сеттера/геттера были непересекающиеся типы.

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

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

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

Например, у меня есть что-то вроде этого:

class Vector3 { /* ... */ }

type XYZ = Vector3 | [number, number, number] | {x: number, y: number, z: number}
type PropertyAnimator = (x: number, y: number, z: number, timestamp: number) => XYZ
type XYZSettables =  XYZ | PropertyAnimator

export class Transformable {
        // ...

        set position(newValue: XYZSettables) {
            this._setPropertyXYZ('position', newValue)
        }
        get position(): Vector3 {
            return this._props.position
        }

        // ...
}

И, как вы можете себе представить, использование очень гибкое:

const transform = new Transformable

// use an array
transform.position = [20, 30, 40]

// use an object
transform.position = {y: 30, z: 40} // skip `x` this time

// animate manually, a property directly
requestAnimationFrame((time) => {
  transform.position.x = 100 * Math.sin(time * 0.001)
})

// animate manually, with an array, which could be shared across instances
const pos = [10, 20, 30]
requestAnimationFrame((time) => {
  pos[2] = 100 * Math.sin(time * 0.001)
  transform.position = pos
})

// Animate with a property function
transform.position = (x, y, z, time) => [x, y, 100 * Math.sin(time * 0.001)]

// or a simple increment:
transform.position = (x, y, z) => [x, y, ++z]

// etc

// etc

// etc

Это 4 года. Как это до сих пор не исправлено? Эта функция, как упоминалось в комментариях выше, имеет большое значение! По крайней мере рассмотреть вопрос о повторном открытии вопроса?

И я полностью согласен с @kitsonk :

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

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

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

Похоже, @RyanCavanaugh дал самое конкретное опровержение этой функции 3 года назад . Интересно, могут ли недавно опубликованные варианты использования DOM, а также доступность нового синтаксиса объявления позволить принять новое решение?

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

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

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

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

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

По крайней мере, я думаю, что Design Limitation здесь можно было бы удалить, я не думаю, что это более актуально.

Это действительно затрудняет использование прокси:
https://stackoverflow.com/questions/57948140/typescript-proxy-that-returns-a-other-type-from-get-than-it-takes-for-set

Видимо, это и есть причина того, что per #33749 .style.display = ...something nullable... больше не проверяет тип; который представлен как проблема корректности обнуляемости. Это немного неискренне, не так ли? Раздражает необходимость выяснять новые критические изменения, когда они ошибочно помечены как исправления ошибок (я искал реальные проблемы, которые это могло вызвать). Лично я нахожу гораздо менее удивительным, что null имеет специальное поведение «использовать по умолчанию», чем пустая строка; и до typecipt 3.7 я предпочитал использовать null для этого. В любом случае было бы неплохо, если бы преднамеренно неправильные аннотации типов, сделанные для обхода этого ограничения, были четко помечены как таковые, чтобы сэкономить время на сортировке проблем с обновлением.

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

Мой вариант использования для этого: у меня есть API, в котором прокси-сервер возвращает обещание, но операция набора не устанавливает обещание. Я не могу сейчас описать это на TypeScript.

let post = await loadPost()
let user = await loadUser()
post.author = user // Proxy handles links these two objects via remote IDs
await save(post)

// Somewhere else in code
let post = await loadPost()
let author = await post.author

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

Им действительно стоит убрать №6 и №7 из этого списка (https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#goals)

Моя проблема

Я хотел бы поместить элемент в массив в методе set и получить весь массив в методе get. Пока мне придется использовать set myArray(...value: string[]) , который отлично работает. Много раз сталкивался с этой проблемой... рассмотрите возможность ее удаления. Информация или предупреждение отлично подойдут для этого.

Пример (что я хотел бы сделать)

class MyClass {
   _myArray: string[] = [];

   set myArray(value: string) {
      this._myArray.push(value);
   }

   get myArray(): string[] {
      return this._myArray;
   }
}

Пример (что я должен сделать)

class MyClass {
   _myArray: string[] = [];

   set myArray(...value: string[]) {
      this._myArray.push(value[0]);
   }

   get myArray(): string[] {
      return this._myArray;
   }
}

Одно решение, которое я придумал, когда мне это было нужно, состояло в том, чтобы установить для свойства значение union type . Это связано с тем, что мое время поступает в виде строки из API, но мне нужно установить его как объект Moment для моего пользовательского интерфейса.

Мой пример

class TimeStorage {
    _startDate: string | Moment = ""
    _endDate: string | Moment = ""

    set startDate(date: Moment | string) {
        this._startDate = moment(date).utc()
    }
    get startDate(): Moment | string {
        return moment.utc(this._startDate)
    }

    set endDate(date: Moment) { 
        this._endDate = moment.utc(date)
    }
    get endDate() { 
        return moment.utc(this._endDate)
    }
}

Я немного опоздал на вечеринку, но недавно сам столкнулся с этой проблемой. Это интересная дилемма, поэтому я немного повозился с ней.

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

class Foo {
    constructor() {
        this._bar = '';
        this.baz = '';
    }

    // error: 'get' and 'set' accessor must have the same type.(2380)
    get bar(): string {
        return this._bar;
    }

    // error: 'get' and 'set' accessor must have the same type.(2380)
    set bar(value: string | number) {
        this._bar = value.toString();
    }

    public baz: string;
    private _bar: string;
}

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

/**
 * Abstract base class for properties.
 */
abstract class Property<GetType, SetType> {
    abstract get(): GetType;
    abstract set(value: SetType): void;
}

/**
 * Proxify an object so that it's get and set accessors are proxied to
 * the corresponding Property `get` and `set` calls.
 */
function proxify<T extends object>(obj: T) {
    return new Proxy<any>(obj, {
        get(target, key) {
            const prop = target[key];
            return (prop instanceof Property) ? prop.get() : prop;
        },
        set(target, key, value) {
            const prop = target[key];
            if (prop instanceof Property) {
                prop.set(value);
            } else {
                target[key] = value;
            }
            return true;
        }
    });
}

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

class Bar extends Property<string, string | number> {
    constructor(bar: string | number) {
        super();
        this.set(bar);
    }

    get(): string {
        return this._bar;
    }

    set(value: string | number) {
        this._bar = value.toString();
    }

    private _bar: string;
}

class Foo {
    constructor() {
        this.bar = new Bar('');
        this.baz = '';
    }

    public bar: Bar;
    public baz: string;
}

И тогда, используя этот класс, вы получите безопасный метод получения и установки для bar :

const foo = new Foo();

// use property's typed setter
foo.bar.set(42);
foo.baz = 'foobar';

// use property's typed getter
// output: 42 foobar
console.log(foo.bar.get(), foo.baz);

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

const foo = new Foo();

// use proxified property setters
Object.assign(proxify(foo), { bar: 100, baz: 'hello world' });

// use property's typed getter
// output: 100 hello world
console.log(foo.bar.get(), foo.baz);

// use proxified property getters
// output: {"bar":"100","baz":"hello world"}
console.log(JSON.stringify(proxify(foo)));

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

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

Если мы добавим это в воображаемую библиотеку:

/**
 * Create a property with custom `get` and `set` accessors.
 */
function property<GetType, SetType>(property: {
    get: () => GetType,
    set: (value: SetType) => void
}) {
    const obj = { ...property };
    Object.setPrototypeOf(obj, Property.prototype);
    return obj;
}

Получаем реализацию, которая немного ближе к исходному классу, а главное имеет доступ к this :

class Foo {
    constructor() {
        this._bar = '';
        this.baz = '';
    }

    public bar = property({
        get: (): string => {
            return this._bar;
        },
        set: (value: string | number) => {
            this._bar = value.toString();
        }
    });

    public baz: string;
    private _bar: string;
}

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

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

Было бы идеально изменить здесь тип значения с boolean на boolean|'', чтобы соответствовать набору значений, которые фактически принимаются установщиком. TypeScript требует, чтобы и геттер, и сеттер имели один и тот же тип, поэтому, если геттер должен возвращать логическое значение, тогда сеттер застревает с более узким типом.... Angular поддерживает проверку более широкого, более разрешительного типа для @Input(), чем это объявляется для самого поля ввода. Включите это, добавив статическое свойство с префиксом ngAcceptInputType_ в класс компонента:

````
класс SubmitButton {
частный _disabled: логическое значение;

отключиться(): логическое {
вернуть это._disabled;
}

установить отключено (значение: логическое значение) {
this._disabled = (значение === '') || стоимость;
}

статический ngAcceptInputType_disabled: boolean|'';
}
````

Пожалуйста , исправьте это.

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

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

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

Мы создаем библиотеку DOM для NodeJ, которая соответствует спецификации W3C, но проблема с Typescript делает это невозможным. Любое обновление?

Меня шокирует, что некоторые члены основной команды TypeScript выступают против функции, которая НЕОБХОДИМА для воссоздания одной из наиболее часто используемых библиотек JavaScript на планете: DOM.

Нет другого способа: а) реализовать многие части спецификации W3C DOM, б) используя систему типов TypeScript (если только вы не прибегаете к повсеместному использованию any , что противоречит всей цели TypeScript).

Домашняя страница typescriptlang.org в настоящее время неверна, когда на ней указано:

TypeScript — это типизированный надмножество JavaScript, который компилируется в обычный JavaScript.

Невозможность воссоздать спецификацию DOM JavaScript показывает, что TypeScript по-прежнему является подмножеством Javascript, а НЕ надмножеством .

Что касается реализации этой функции, я согласен с @ gmurray81 и многими другими, которые утверждали, что тип геттера должен быть подмножеством типа объединения сеттеров. Это гарантирует отсутствие несвязной типизации. Такой подход позволяет функции установщика очищать входные данные, не разрушая тип получателя (т. е. заставляя прибегать к использованию any ).

Вот что я не могу сделать. Что-то простое, что я сделал подобное в JS, но не могу в TS.

4й вопрос, который ДЕЙСТВИТЕЛЬНО можно реализовать.

export const enum Conns {
  none = 0, d = 1, u = 2, ud = 3,
  r = 4, rd = 5, ru = 6, rud = 7,
  l = 8, ld = 9, lu = 10, lud = 11,
  lr = 12, lrd = 13, lru = 14, lrud = 15,
  total = 16
}

class  Tile {
  public connections = Conns.none;

  get connLeft() { return this.connections & Conns.l; };

  set connLeft(val: boolean) { this.connections = val ? (this.connections | Conns.l) : (this.connections & ~Conns.l); }
}

Сейчас столкнулся с этой проблемой. Нижеприведенное кажется разумным решением, цитируя @calebjclark :

Что касается реализации этой функции, я согласен с @ gmurray81 и многими другими, которые утверждали, что тип геттера должен быть подмножеством типа объединения сеттеров. Это гарантирует отсутствие несвязной типизации. Такой подход позволяет функции установщика очищать входные данные, не разрушая тип получателя (т. е. не прибегая к использованию any).

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

При этом классы неявно также являются интерфейсами, а геттеры и сеттеры по сути являются деталями реализации, которые не будут отображаться в интерфейсе. В примере OP вы получите type MyClass = { myDate(): moment.Moment; } . Вам нужно каким-то образом выставить эти новые типы геттера и сеттера как часть интерфейса, хотя лично я не понимаю, почему это желательно.

Эта проблема в основном требует менее ограниченной версии перегрузки операторов = и === . (Геттеры и сеттеры уже являются перегрузкой операторов.) И как человек, имевший дело с перегрузками операторов в других языках, я скажу, что в большинстве случаев их следует избегать. Конечно, кто-то мог бы предложить какой-нибудь математический пример, например, декартовы и полярные точки, но в подавляющем большинстве случаев, когда вы будете использовать TS (включая примеры в этой теме), я бы сказал, что необходимость перегрузки оператора, вероятно, является запахом кода. Может показаться, что это упрощает API, но на самом деле все наоборот. Как упоминалось ранее, если следующее не обязательно верно, это очень сбивает с толку и неинтуитивно.

foo.x = y;
if (foo.x === y) { // this could be false?

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

@МайкиБуркман

[...] но в подавляющем большинстве случаев, когда вы будете использовать TS (включая примеры в этой ветке), я бы сказал, что необходимость перегрузки оператора, вероятно, является запахом кода [...]

 foo.x = y; 
 if (foo.x === y) { // this could be false?

Таким образом, простое совпадение типов не предотвращает неожиданного поведения, и действительно el.style.display = ''; //so is this now true: el.style.display === ''? показывает, что это также не является теоретическим. Между прочим, даже со старыми полями это предположение не выполняется для NaN.

Но что еще более важно, ваш аргумент игнорирует тот факт, что TS не может быть уверен в этих вещах, потому что TS необходимо взаимодействовать с существующими API-интерфейсами JS, включая основные и вряд ли изменяющие такие вещи, как DOM. И поэтому просто не имеет значения, идеален ли API; важно то, что TS не может чисто взаимодействовать с таким API . Теперь вы вынуждены либо повторно реализовать любую резервную логику, которую API использовал внутренне для принуждения значения вне типа геттера, переданного в сеттер, и, таким образом, ввести свойство как тип геттера, либо игнорировать проверку типов, чтобы добавить бесполезные утверждения типа на каждом сайте геттеров и, таким образом, введите свойство как тип сеттеров. Или, что еще хуже: делайте все, что TS решит аннотировать для DOM, независимо от того , идеально ли это.

Все эти варианты плохи. Единственный способ избежать этого — смириться с необходимостью максимально точно представлять JS API, и здесь: это кажется возможным (по общему признанию, без каких-либо знаний о внутренностях TS, которые могут сделать это явно нетривиальным).

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

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

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

@MikeyBurkman вопрос приоритета актуален. Меня больше всего беспокоит решение @RyanCavanaugh закрыть этот вопрос и списать его со счетов. Отказ от всех проблем, указанных в этой теме, прямо противоречит заявленной миссии Typescript, которая должна быть «надмножеством» Javascript (а не подмножеством).

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

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

Я думаю, что вы недооцениваете важный аспект TypeScript, поскольку он должен сделать более безопасным использование существующих API-интерфейсов DOM и существующих API-интерфейсов JS. Он существует для того, чтобы держать в безопасности не только ваш собственный код.

Вот моя точка зрения, я создаю библиотеки компонентов, и хотя я не обязательно преднамеренно вызывал бы это несоответствие между сеттерами/геттерами, учитывая мои драмтеры. Иногда моя рука вынуждена из-за того, как мои библиотеки должны взаимодействовать с другими системами на месте. Например, Angular устанавливает все входные свойства компонента, поступающие из разметки, в виде строк, вместо того, чтобы выполнять какое-либо приведение типов на основе их знания целевого типа (по крайней мере, в последний раз, когда я проверял). Итак, вы отказываетесь принимать строки? Делаете ли вы все строки, даже если это сделает ваши типы ужасными в использовании? Или вы делаете то, что хотел бы TypeScript, и используете такой тип, как: string | Цвет, но сделать использование геттеров ужасным. Все это довольно ужасные варианты, которые снижают безопасность, тогда как некоторая дополнительная выразительность системы типов могла бы помочь.

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

Посмотрите немного вверх: Angular намного хуже, чем вы думаете .

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

class Store {
  private dict: Map<string, any>;

  get name(): string | null {
    return this.dict.get('name') as string | null;
  }

  set name(value: string) {
    this.dict.set('name', value);
  }
}

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

@fan-tom, отличный пример, почему эту проблему нужно снова открыть! Пусть они придут.

Эта проблема имеет чрезвычайно большое количество голосов!

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

Домашняя страница typescriptlang.org в настоящее время неверна, когда на ней указано:

TypeScript — это типизированный надмножество JavaScript, который компилируется в обычный JavaScript.

TypeScript — это типизированный _superset_ _subset_ JavaScript, который компилируется в обычный JavaScript. :смайлик:

TypeScript — это типизированный надмножество подмножества JavaScript, которое компилируется в обычный JavaScript.

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

Они говорят: «Мы не будем это моделировать, потому что нам это сложно сделать, и мы считаем, что вы все равно не должны этого делать». Но JavaScript полон вещей, которые вы _не должны_ делать, и, как правило, Typescript не будет мешать вам делать эти вещи, если у вас есть воля и ноу-хау, потому что, похоже, общая стратегия заключается в том, чтобы не высказывать свое мнение о том, что такое JavaScript. вы можете и не можете просто работать как Typescript.

Вот почему так странно отказываться от моделирования этого распространенного (используемого в DOM!) сценария, ссылаясь на убеждение, что API не должны выполнять принуждение на основе сеттера, как на обоснование этого.

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

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: moment.Moment) {
        assert.fail('Setter for myDate is not available. Please use: setMyDate() instead');
    }

    setMyDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

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

constructor(value: Date | moment.Moment) {
    this.setMyDate(value);
}

Проблема при прямом присвоении значений все еще остается.

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

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

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

@xhliu ... слишком сложно для решения ...

https://ts-ast-viewer.com/#code/MYewdgzgLgBFCm0DyAjAVjAvDA3gKBkJgDMQQAuGAIhQEManKvAXzzwWXQDpSQg

const testObj = {
    foo: "bar"
}

testObj.foo

Символ foo говорит следующее:

  foo
    flags: 4
    escapedName:"foo"
    declarations: [
      PropertyAssignment (foo)
    ]
    valueDeclaration: PropertyAssignment (foo)

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

Спекулятивный с этого момента:

Если бы можно было объявить (для примера псевдокода) accessDelclaration: AccessSpecification (foo) , то PropertyAccessExpression , который знает о символе foo и его объявлениях, мог бы условно проверить, есть ли «accessDelclaration» и вместо этого используйте ввод из этого.

Предполагая, что существует синтаксис для добавления этого свойства accessDelclaration к символу "foo", PropertyAccessExpression должен иметь возможность извлекать "AccessSpecification" из символа, который он получает от ts.createIdentifier("foo") , и выдавать различные типы.

ts.createPropertyAccess(
  ts.createIdentifier("testObj"),
  ts.createIdentifier("foo")
)

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

Написано много хороших примеров, почему это крайне необходимо (особенно для DOM и Angular).

Я просто добавлю, что я столкнулся с этим сегодня при переносе старого кода JS в TS, где назначение string на window.location не сработало, и мне пришлось использовать обходной путь as any 😟

Доступное только для чтения свойство Window.location возвращает объект Location с информацией о текущем расположении документа.

Хотя Window.location является объектом Location только для чтения, вы также можете присвоить ему DOMString. Это означает, что в большинстве случаев вы можете работать с местоположением, как если бы оно было строкой: location = ' http://www.example.com ' является синонимом location.href = ' http://www.example.com '. .
источник

перенос старого кода JS в TS, где назначение string на window.location не сработало, и мне пришлось использовать обходной путь as any

Это отличный пример.

ТС это нужно. Это вполне нормальная часть JavaScript.

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

@AGluk , этот вопрос нужно открыть заново.

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