Typescript: 建议:允许 get/set 访问器为不同类型

创建于 2015-03-26  ·  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 getter 和 setter 完全有效并且效果很好,我相信这是此功能的主要优点/目的。 不得不提供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 中与普通属性没有任何不同,因为从那个角度来看它们看起来是一样的。 这意味着 getter 和 setter 之间没有区别,因此无法 a) 要求实现使用访问器而不是单个实例成员,并且 b) 指定 getter 和 setter 之间的类型差异。

谢谢你的快速回复,丹。 我将遵循不太优雅的方式。 感谢您做的伟大工作!

具有不同类型的 JavaScript getter 和 setter 完全有效并且效果很好,我相信这是此功能的主要优点/目的。 不得不提供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 getter 和 setter 完全有效并且效果很好,我相信这是此功能的主要优点/目的。

这是在争论 JavaScript 是弱类型的,所以 TypeScript 应该是弱类型的。 :-S

这是在争论 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很高兴知道,谢谢! 看起来我只需要为 ST 更新我的 IDE 插件编译器。

@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(value%3A%20any)%20%7B%0A% 09%20%20%0A%20%20%7D%0A%7D

我刚刚用 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 linter 后,它不再抛出错误,即使用 TypeScript 1.7.x。 我一直假设它是 1.7+ 中即将推出的增强功能,所以也许 1.8.0 已经退步了。

即使使用支持 typescript 1.7.5 并且在我的机器上全局安装 typescript 1.7.5 的 Visual Studio 代码版本(0.10.5(2015 年 12 月)),这仍然是一个问题:

image

那么这将支持什么版本呢?
谢谢

我认为丹错了。 getter 和 setter 的类型必须相同。

耻辱。 在编写用于量角器测试的页面对象时,这将是一个很好的功能。

我本来可以写一个量角器测试:

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怎么样?

getter 将返回一个webdriver.promise.Promise<string>而我想分配一个string值的 setter。

也许以下更长的量角器测试形式更清楚:

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

@RyanCavanaugh通过引入 null 注释,这可以防止允许调用带有 null 的 setter 将其设置为某个默认值的代码。

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的例子很有说服力,但是今天的类型系统将 getter/setter 视为常规属性。 为此,两者应该具有相同的值。 支持读/写类型将是一个很大的变化,我不确定这里的实用程序是否值得实施成本。

虽然我喜欢 TypeScript 并感谢团队为它付出的所有努力(真的,你们太棒了!),但我必须承认我对这个决定感到失望。 这将是比getFoo()/setFoo()get foo()/set foo()/setFooEx()的替代方案更好的解决方案。

只是一个简短的问题列表:

  • 我们目前假设属性只有一种类型。 我们现在需要区分每个位置的每个属性的“读取”类型和“写入”类型
  • 所有类型关系都变得更加复杂,因为我们必须对每个属性而不是一种类型进行推理(是否可以将{ get foo(): string | number; set foo(): boolean }分配给{ foo: boolean | string | number } ,反之亦然?)
  • 我们目前假设一个属性在设置后仍然具有下一行中设置值的类型(这在某些人的代码库中显然是一个错误的假设,wat?)。 我们可能只需要“关闭”对此类属性的任何流控制分析

老实说,我真的尽量避免在这里规定如何编写代码,但我真的反对这种代码的想法

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

应该使用试图具有健全语义的语言打印除"hello"之外的任何内容。 属性应该具有与字段和外部观察者无法区分的行为。

@RyanCavanaugh虽然我同意你关于注入的观点,但我可以看到一个反驳论点,即_可能_只是非常 TypeScripty... 在 setter 中抛出一些弱类型的东西,但总是返回强类型的东西,例如:

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)

所以 setter 是string | null | undefined而 getter 只是string 。 我们应该在这个库的类型定义中使用什么类型? 如果我们使用前者,编译器将需要到处进行无用的空检查,如果我们使用后者,我们将无法将null分配给该属性。

我有另一个示例,我希望 getter 返回可为空的,但 setter 绝不应允许 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!

我知道构建一些特殊的逻辑来允许 getter/setter 在 null 上有所不同可能需要做太多的工作,这对我来说并不是什么大不了的事,但拥有它会很好。

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

在我看来,具有非空检查的好处伴随着陷阱,这就是其中之一。 关闭严格的空检查,这是可能的,但是您没有得到编译器的帮助来防止空引用异常。 但是,如果您需要编译器帮助,我认为应该提供更多支持,例如为 getter 和 setter 分别定义至少可空性(如果没有别的)。

该问题上的标签表明这是一个设计限制,并且实施将被认为过于复杂,这本质上意味着如果有人有一个非常令人信服的理由说明为什么会出现这种情况,那么它不会去任何地方。

@mhegazy @kitsonk我可能有偏见,但我觉得这是一个针对常见模式的严格空值检查而出现的错误,特别是在其他大括号(如尚未进行空值检查的语言)中。 一种解决方法将要求消费者使用 bang 运算符或检查它实际上永远不会为空(这是使用默认值永远不会为空的点)。

一旦您添加了严格的空检查,这就会崩溃,因为现在它在技术上是不同的类型。 我不是要求设置强的不同类型,但似乎启用此功能的设计要求也将启用强的不同类型。

对于弱类型,可以使用另一种设计,这样如果接口定义和 d.ts 文件不想启用完全不同的类型,那么像 null 和 undefined 这样的类型将是特殊情况。

回应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_ 的转换类型,我个人喜欢看到方法被调用,对消费者来说更明确的是,会发生某种 ✨ 并生成只读 getter 来检索值。

这并不意味着不存在现实世界的使用,这可能是理智和合理的。 我想这涉及到将整个类型系统分成两部分的复杂性,其中类型必须根据它们的读取和写入条件进行跟踪。 这似乎是一个非常重要的场景。

我同意 :sparkles: 在这里,但我不是在争论或反对使用该模式,我只是想跟随已经使用它的代码并描述它的形状。 它已经按照它的工作方式工作了,而且 TS 没有给我描述它的工具。

无论好坏,JS 让我们能够将赋值转换为函数调用,并且人们正在使用它。 没有能力为set/get对分配不同的类型,为什么在环境类型中甚至有set/get呢? 如果 Salsa 总是将myThing.foo视为单一类型的成员变量,而不管它在赋值的哪一侧,它就不必知道属性是用 getter 和 setter 实现的。 (显然,实际的 TypeScript 编译完全是另一回事。)

@thw0rted

看起来有一些人在看这个帖子,他们对 TypeScript 更熟悉,所以也许还在关注的人可以回答一个相关的问题。 在这个 Cesium 问题上,我提到了我们在这里讨论的 get/set 类型限制,Cesium 的人说他们遵循的模式来自 C#——它是隐式构造函数。

C# 的隐式用户定义转换运算符根据值的类型执行静态代码生成。 TypeScript 类型被删除并且不影响运行时行为( async / await边缘情况对于Promise polyfillers 不耐受)。

@kitsonk我不得不在属性方面大体上不同意。 在 Java、C++ 和 C# 上花费了相当多的时间后,我非常喜欢属性(如在 C# 中所见),因为它们提供了关键的句法抽象(这在 JavaScript 中有些真实)。 它们允许接口以有意义的方式分离读/写功能,而无需更改语法。 我讨厌看到动词浪费在像getX()这样的琐碎操作上,因为X可能是隐含的。
IMO 令人困惑的 API,其中许多都像您所说的那样滥用访问器,更多地源于太多的 _setter_ 做神奇的事情。

如果我有一个只读但实时查看某些数据的视图,比如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 不是旨在帮助开发人员相互交流吗?

此外,setter 被称为 Mutators 是有原因的。 如果我不需要任何类型的转换,我不会使用 setter,我会自己设置变量。

将 javascript 项目移植到打字稿。 我遇到了这个问题。。

这很适合用在有角度的 @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;
  }
}

我们也是吗? 我真的很想有可能用 getter 而不是 setter 返回不同的类型。 例如:

class Field {
  private _value: string;

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

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

这就是 99% 的原生实现所做的(如果你将一个数字传递给(input as HTMLInputElement).value ,它总是会返回一个字符串。事实上, getset应该被视为方法,并且应该允许一些:

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 问题想出一些不完全繁琐的解决方法,我个人会认为这个问题已经解决。 我仍然是一个 TS 新手,所以也许我当前的解决方案(强制转换或不必要的类型保护)不是对现有功能的最佳使用?

同意。 当类型必须相同时, 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));
  }

我希望存储的值是一个数字,但我想在 setter 中从字符串转换为浮点数,所以我不必每次设置属性时都进行 parseFloat 。

我认为结论是,这一个有效的(或至少相当被接受的)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,以便预处理器 (?) 在将 TS 转译为 JS 之前,将所有读取的w.price转换为显式转换。 当然,我不知道 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: numberset price(val)

是的,我也在这里,因为 Angular 对组件的使用似乎希望我们有不同的 getter/setter 类型。

如果您从标记(除非使用绑定表达式)设置组件属性(通过属性),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 允许我们对与碰巧使用我们的类型的外部库的交互进行建模,从而违反读/写类型相等公理,这将很有帮助。

在这种情况下,在 getter 上使用联合类型会很成问题,因为它需要各种烦人的类型保护/断言。 但是将联合类型从设置器中移除感觉是错误的,因为任何工具都可能决定开始尝试验证从属性设置的属性应该可以从字符串分配。

因此,在这种情况下,类型系统的表达能力不足以捕捉外部库如何被允许使用该类型。 这并不_立即_重要,因为外部库实际上并没有在打字稿级别使用这些类型以及类型信息。 但最终可能很重要,因为工具很可能会消耗类型信息。

上面的一个人提到了另一种解决方案,它看起来有点令人讨厌,但可能可行,我们可以将联合类型用于 setter/getter,但有一些方法可以表明联合中的某些类型对于 getter 是不可能的。 因此,实际上,在 getter 调用站点站点控制流程中已预先从考虑中删除,就好像有人使用类型保护来检查它们不存在一样。 不过,与允许 setter 上的超集联合与 getter 上的子集联合相比,这似乎很烦人。

但也许这是一种在内部解决问题的方法。 只要 setter 只是 getter 类型的超集联合。 在内部将 getter 和 setter 的类型视为等效,但将 getter 的联合类型的部分标记为不可能。 因此,控制流分析将它们排除在考虑之外。 这会绕过设计限制吗?

对以上内容进行详细说明。 从类型表达的角度来看,能够将复合类型的某些部分表示为不可能的,也许这将是一件有用的事情。 这不会影响与具有相同结构但没有不可能修饰符的另一种类型的相等性。 不可能的修改器只会影响控制流分析。

作为替代建议,如果有一些语法可以将用户定义的类型保护应用于返回值,这也足够了。 我意识到,在正常情况下,这有点荒谬,但是在返回值和参数上具有这种表现力将有助于解决这些边缘情况。

用户在返回值上定义的类型保护,还具有在压缩到属性时可以在类型声明/接口中表达的好处?

只需在此处添加另一个用例,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;
}

我认为这不能解决手头的问题。 使用 getter 时仍然需要类型保护,尽管实际上,getter 只返回联合类型的子集。

我正在使用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

@keenndrums这正是我们希望访问器具有不同类型的原因。 我发现的是一种解决方法,而不是解决方案。

@jpidelatorre我目前的解决方法是使用另一个函数作为 setter

export class FooBar {
  private bar: string;

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

  setFoo (value: Date | string ) {}
}

@keenondrums不像访问器那样好看,但还是最好的选择。

除非您为 getter 使用单独的函数,否则这并不是 Angular 组件上的@input属性所发生的事情的真正模型。

这太丑陋了。

我也想要这个功能,但需要它在没有变通方法的 .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 允许您使用与接口匹配的对象设置属性,但 getter 总是返回一个真实的类实例:

interface IFoo {
    bar: string;
}

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

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

我基本上已经忘记了这个问题,但是在阅读它时我突然想到了一个想法。 我认为我们已经确定这值得抽象地做,但在技术上太复杂以至于不可行。 这是一个权衡计算——这在技术上并非不可能,只是不值得花费时间和精力(并增加代码复杂性)来实现。 正确的?

这使得无法准确描述核心 DOM API 的事实是否完全改变了数学? @RyanCavanaugh

我真的反对这样的想法,即这段代码foo.bar = "hello"; console.log(foo.bar);应该在试图具有健全语义的语言中打印除“hello”之外的任何内容。

我们可以争论是否应该以这种方式使用它,但 DOM 一直支持像el.hidden=1; console.log(el.hidden) // <-- true, not 1这样的结构。 这不仅仅是少数人使用的模式,也不仅仅是它在一个流行的库中,所以支持它可能是一个好主意。 这是 JS 一直运作的核心原则——在语言的灵魂中融入了一点 DWIM——在语言级别上使其不可能打破了 TS 的基本原则,即它必须是 JS 的“超集” . 这是一个从维恩图中伸出的丑陋气泡,我们不应该忘记它。

这就是为什么我仍然喜欢保持 setter/getter 具有相同类型的想法:

number | boolean

但是引入某种类型可以表明,虽然 getter 在技术上具有相同的联合类型,但实际上它只会返回联合中类型的子集。 不是把它当作 getter 上的某种窄化保护(推理流的东西?)使它比对类型模型的修改更直接吗? (他说对内部结构一无所知......)

或者,如果 getter 上使用的类型是 setter 类型的严格子集,这可能只是暗示吗?

或者,如果 getter 上使用的类型是 setter 类型的严格子集,这可能只是暗示吗?

👍!

我认为我们都同意你不应该得到一个不可设置的类型; 我只是想要某种方法来自动缩小get上的类型,使其成为我知道它将永远存在的类型。

我不明白为什么有些人反对它会违反类型安全等等。 如果我们允许至少设置器具有不同的类型,我认为它不会违反任何类型安全,因为我们如何检查您将不允许其他类型设置为属性。
前任:
假设我有一个属性为 Array但从 DB 中,这将作为逗号分隔的字符串返回,例如“10,20,40”。 但是我现在无法将其映射到 mode 属性,所以如果您可以允许这样会非常有帮助

私人 _employeeIdList : number[]

获取 EmployeeIDList(): number[] {
返回 this._employeeIdList ;
}
设置员工IDList(_idList:任何){
if (typeof _idList== 'string') {
this._employeeIdList = _idList.split(',').map(d => Number(d));
}
否则 if (typeof _idList== 'object') {
this._employeeIdList = _idList as number[];
}
}

我会很容易地解决这个问题并且它是完全类型安全的,即使您在 SET 中允许使用不同的类型,但它仍然阻止我们为属性分配错误的类型。 所以赢赢。 我希望团队成员放下自尊心,试着思考它造成的问题并解决这个问题。

我想补充一点,我仍然认为这对于对现有 JS 库的行为进行建模非常重要。

虽然抬起鼻子并定义一个将类型强制为传入类型的子集并始终在 getter 中返回该子集作为代码气味的 setter 很好,但实际上,这是 JS 领域中相当常见的模式.

我认为 TypeScript 很可爱,因为它允许我们在描述现有 JS API 的复杂性时非常富有表现力,即使它们没有理想的结构化 API。 我认为这是一个场景,如果 TypeScript 的表现力足以让我们指出 getter 将始终返回一个类型,该类型是 getter 类型的严格子集,这将为现有 API 建模增加巨大的价值,甚至DOM!

Trusted Types是一个新的浏览器 API 提议,用于对抗 DOM XSS。 它已经在 Chromium 中实现(在标志后面)。 API 的大部分修改了各种 DOM 属性的设置器以接受可信类型。 例如, .innerHTML接受TrustedHTML | string而它总是返回string 。 为了在 TypeScript 中描述 API,我们需要修复这个问题。

与前面评论的不同之处在于,这是一个浏览器 API(而不是用户级库),不能轻易更改。 此外,将Element.innerHTML的类型更改为any (这是目前唯一可能的解决方案)的影响比不精确地描述用户空间库的影响更大。

是否有可能重新打开此拉取请求? 还是我错过了其他解决方案?

抄送: @mpprobst ,@koto。

在像 TypeScript 这样支持类型联合的语言中,这个特性是自然而然的,也是一个卖点。

@RyanCavanaugh即使 getter 和 setter 具有相同的类型,也不能保证o.x === y o.x = y作为 setter 可以在保存值之前进行一些清理。

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

我赞同@vrana 的关注。 当前的行为使得无法在 Typescript 中对一些现有的 API 进行建模。

对于 Web API 来说尤其如此,其中很多的 setter 和 getter 类型是不同的。 在实践中,Web API 设置器执行各种类型强制,其中一些直接指定给给定的浏览器功能,但大多数通过IDL隐式指定。 许多设置器也会改变值,请参见例如位置接口规范。 这不是单个开发人员的错误——它是 Web 开发人员编写代码时所针对的 API 规范。

缩小 getter 类型允许 Typescript 表示这些 API,这现在是不可能的。

您可以表示这些 API,因为可以正确地说属性的类型是您可以提供给 setter 或从 getter 获取的可能类型的联合。

以这种方式描述 API 只是不_高效_。 您要求消费者在他们使用 getter 来缩小可能类型的每个实例中都使用类型保护。

如果你没有明确地知道你总是会从 getter 返回一个缩小的类型,那没关系,因为许多 Web API 甚至锁定在它们的规范中。

但即使暂时搁置这一点,并谈论用户 API,在setter上接受联合类型的一个强大用例是“脚本化”用例。 我们希望接受一系列离散类型,我们可以接受强制转换为我们真正想要的类型。

为什么允许这样做? 便于使用。

这对于您在内部为自己的团队使用而开发的 API 可能无关紧要,但对于为公共、通用消费而设计的 API 可能很重要。

TypeScript 可以让我们精确地描述一个 API,该 API 放宽了它的某些属性可接受的类型,但这种好处被另一端的摩擦所破坏,因为需要过多的类型检查/保护来确定 getter 返回类型,我们更愿意_specify_。

我认为这与@RyanCavanaugh假设的理想化案例不同。 这种情况意味着您的 getter 和 setter 应该始终具有相同的联合类型,因为您的支持字段也具有相同的联合类型,并且您将始终只是往返该值,并且更改它的类型只是荒谬的。

我认为这个案例围绕着对联合类型的更理想化的使用,您正在处理构造类型,并且确实将该联合类型视为一个语义单元,您可能应该为其创建一个别名。

type urlRep = string | Url;

大多数事情只会往返它,只处理常见的道具,在某些情况下,你会用一些类型守卫打破黑匣子。

我认为这与我们在这里描述的情况完全不同。 我们所描述的是,通用/公共使用 API,尤其是在 JavaScript 等脚本语言中使用的 API 经常故意放宽 setter 可接受的类型,因为它们会同意将一系列类型强制转换为理想类型,所以他们提供它作为生活质量的改善来接受所有这些。

如果你既是 API 的生产者又是消费者,这种事情可能看起来很荒谬,因为它只会做更多的工作,但如果你正在设计一个面向大众消费的 API,则可能会很有意义。

而且我认为没有人会希望 setter/getter 具有 _disjoint_ 类型。 这里讨论的是 API 生产者向 API 消费者断言 getter 将返回一个类型,该类型是 setter 联合类型的严格子集。

而且我认为没有人会希望 setter/getter 具有不相交的类型。

只是为了提供一个相反的观点。 我肯定想要这样,因为 setter/getter 的不相交类型在 javascript 中是完全合法的。 而且我认为主要目标应该是使类型系统具有足够的表现力,以正确键入任何在 javascript 中合法的内容(至少在 .d.ts 级别)。 我知道这不是一个好习惯,例如全局对象也不是一个好习惯,也不是更改函数等内置函数的原型,但是我们仍然可以在 .d.ts 文件中输入那些就好了,我还没有听到有人后悔我们可以做到这一点(即使它确实会导致一些设计不佳的类型定义文件出现在确定类型上)。

刚刚遇到了一种情况,我需要将其作为一个功能,以便拥有我无法控制的正确类型。 setter 需要更加自由并强制转换为可以由 getter 始终返回的更保守的类型。

我真的希望这个功能存在。 我正在将 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 引入了在声明文件中键入访问器的语法,同时保留了 getter 和 setter 的类型必须相同的限制。

我们的 UI 工具包在很大程度上依赖于自动转换的 setter,其中 setter 接受的类型比 getter 返回的单一类型要多。 因此,如果 TS 提供了一种使这种模式类型安全的方法,那就太好了。

看起来@RyanCavanaugh3 年前对这个特性给出了最具体的反驳。 我想知道最近报道的 DOM 用例以及新声明语法的可用性是否可能允许做出新的决定?

是的,一看到这个新功能,我也立刻想到了这个功能请求。 我也希望它用于我们的 UI 框架。 伙计们,这是一个真实的用例。

TSConf 2019将于10 月 11日举行,如果有人有机会,这将是一个很好的问答环节的回顾🤔

我之前可能在这个长线程上说过这个,但我认为它需要重申。

IMO TypeScript 中的表达类型系统有两个不同的目的。 首先是允许您编写更安全的新代码。 如果这是唯一的目的,也许您可​​以提出一个论点,即您可以避免这种用例,因为如果您不允许这种情况,代码可能会更安全(但我认为我们仍然可以对此进行争论)。

然而,第二个目的是按原样捕获库的行为,在 DOM 和 JS 库中,在 setter 中自动强制转换是一种相当普遍的做法,但在 getter 中返回一个预期的类型。 为了让我们在类型系统中捕捉到这一点,我们可以更准确地描述现有框架。

至少,我认为Design Limitation可以在这里删除,我认为这不再适用。

这显然是每个 #33749 .style.display = ...something nullable...不再类型检查的原因; 这是一个可空性正确性问题。 这有点虚伪是不是? 当它们被错误地标记为错误修复时,必须找出新的重大更改是很烦人的(我一直在寻找这可能导致的实际问题)。 就个人而言,我发现null具有“使用默认值”的特殊行为比空字符串具有的特殊行为要少得多。 在 typecipt 3.7 之前,我更喜欢使用 null 来捕获它。 在任何情况下,如果为解决此限制而故意不正确的类型注释被清楚地标记为这样,以节省对升级问题进行分类的时间,那将是很好的。

我也有兴趣为此寻找前进的道路。 如果它只在环境上下文中被允许呢? @RyanCavanaugh会帮助解决您的复杂性问题吗?

我的用例是我有一个 API,其中代理返回一个 Promise,但 set 操作没有设置 Promise。 我现在无法在 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 的字符串,但我需要为我的 UI 设置为 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;
}

然后,使用此类将具有类型安全的 getter 和 setter 用于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);

如果你需要更动态地使用属性 setter 或 getter,你可以使用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|'',以匹配 setter 实际接受的值集。 TypeScript 要求 getter 和 setter 具有相同的类型,所以如果 getter 应该返回一个布尔值,那么 setter 就会被更窄的类型卡住...... Angular 支持为 @Input() 检查更宽、更宽松的类型。为输入字段本身声明。 通过向组件类添加带有 ngAcceptInputType_ 前缀的静态属性来启用此功能:

````
类提交按钮 {
私有_disabled:布尔值;

禁用():布尔{
返回 this._disabled;
}

设置禁用(值:布尔){
this._disabled = (value === '') || 价值;
}

静态 ngAcceptInputType_disabled: boolean|'';
}
````

请,解决这个问题。

是的,我们确实到处都有这种丑陋,以支持 Angular 想要使用的模式。 目前的限制过于自以为是。

如果 TS 想被用来对 web 领域的所有库进行建模,那么它需要努力减少固执己见,否则它将无法对所有设计进行建模。

在这一点上,我认为贡献者有责任沟通为什么这个问题没有被重新打开? 在这一点上是否有一个贡献成员甚至强烈反对这一点?

我们正在为符合 W3C 规范的 NodeJs 构建一个 DOM 库,但是这个 Typescript 问题使它成为不可能。 任何更新?

令我震惊的是,TypeScript 核心团队中的一些人反对一项必需的功能,以便重新创建地球上最常用的 JavaScript 库之一:DOM。

没有其他方法可以 a) 实现 W3C DOM 规范的许多部分,而 b) 使用 TypeScript 的类型系统(除非您到处使用any ,这违背了 TypeScript 的全部目的)。

typescriptlang.org 的主页目前在声明时不准确:

TypeScript 是 JavaScript 的类型化超集,可编译为纯 JavaScript。

无法重新创建 JavaScript 的 DOM 规范表明 TypeScript 仍然是 Javascript 的子集而不是超集

关于实现这个特性,我同意@gmurray81和许多其他人的观点,他们认为getter 的类型必须是setter 的联合类型的子集。 这确保没有脱节的打字。 这种方法允许 setter 的函数在不破坏 getter 类型的情况下清理输入(即,被迫使用any )。

这是我不能做的。 我在 JS 中做过类似的简单事情,但在 TS 中却做不到。

真正可以实施的 4yo 问题。

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和许多其他人的观点,他们认为getter 的类型必须是setter 的联合类型的子集。 这确保没有脱节的打字。 这种方法允许 setter 函数在不破坏 getter 类型的情况下清理输入(即,被迫使用 any)。

因为 getter 是 setter 类型的子集,我认为这是不必要的限制。 实际上,setter 的类型在理论上可以是任何东西,只要实现总是返回 getter 类型的子类型。 要求 setter 是 getter 类型的超类型是一种奇怪的限制,可能会遇到此票证中提出的几个用例,但以后肯定会收到投诉。

话虽如此,类也是隐含的接口,getter 和 setter 本质上是不会出现在接口中的实现细节。 在 OP 的示例中,您最终会得到type MyClass = { myDate(): moment.Moment; } 。 您必须以某种方式将这些新的 getter 和 setter 类型公开为接口的一部分,尽管我个人不明白为什么这样做是可取的。

这个问题基本上是要求====的运算符重载的限制较少的版本。 (Getter 和 setter 已经是运算符重载。)作为处理过其他语言运算符重载的人,我会说我觉得在大多数情况下应该避免它们。 当然,有人可能会建议一些数学示例,例如笛卡尔和极点,但在绝大多数使用 TS 的情况下(包括此线程中的示例),我认为需要运算符重载可能是代码异味。 看起来它使 API 变得更简单,但往往适得其反。 如前所述,以下内容不一定正确是非常令人困惑和不直观的。

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

(我认为我见过的唯一一次似乎合理的 setter 用于记录日志并在对象上设置“脏”标志,以便其他东西可以判断字段是否已更改。这些都没有真正改变目的。)

@MikeyBurkman

[...]但是在绝大多数使用 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 需要与现有的 JS api 互操作,包括主要的和不太可能改变的事情,比如 DOM。 因此,api是否理想并不重要。 重要的是 TS 不能与这样的给定api 完全互操作。 现在,您不得不重新实现 api 在内部使用的任何回退逻辑来强制传递给 setter 的非 getter 类型值,从而将属性键入为 getter 类型,或者忽略类型检查以添加每个 getter 站点上的无用类型断言,因此将属性键入为 setter 类型。 或者,更糟糕的是:做任何 TS 碰巧选择为 DOM 注释的事情,不管是否理想。

所有这些选择都是错误的。 避免这种情况的唯一方法是接受尽可能忠实地表示 JS api 的需要,在这里:这似乎是可能的(诚然,没有任何 TS 内部知识,这可能会使这绝对不简单)。

这似乎是可能的(诚然,在没有任何 TS 内部知识的情况下,这可能使这绝对不是微不足道的)

我怀疑他们最近启用的一些新类型声明语法在以前不太可行的情况下可以表达出来,所以他们真的应该考虑重新讨论这个问题...... @RyanCavanaugh

在理想情况下,TS 将与所有 JS API 互操作。 但事实并非如此。 有一堆 JS 成语在 TS 中打不准。 但是,我宁愿他们专注于修复实际上会导致更好的编程习惯的事情,而不是更糟。 虽然以理智的方式支持这一点会很好,但我个人更希望他们先花有限的时间做其他十几件事。 这在该列表中很靠后。

@MikeyBurkman优先级问题是有效的。 我最关心的是@RyanCavanaugh决定关闭此问题并将其注销。 忽略该线程上指出的所有问题与 Typescript 的既定使命直接冲突,即成为 Javascript 的“超集”(而不是子集)。

是的,我绝对同意这个问题可能不应该被关闭,因为它_可能_最终应该被修复。 (虽然我真的怀疑它会是。)

虽然以理智的方式支持这一点会很好,但我个人更希望他们先把有限的时间花在其他十几件事上

我认为您低估了 TypeScript 的一个重要方面,因为它应该使使用现有 DOM API 和现有 JS API 更安全。 它的存在不仅仅是为了保留您自己的代码,在它的泡沫中,安全。

这是我的观点,我构建了组件库,虽然考虑到我的 druthers,但我不一定会故意导致 setter/getter 之间存在这种不匹配。 有时我的手被迫基于我的库需要如何与其他系统就地交互。 例如,Angular 将来自标记的组件上的所有输入属性设置为字符串,而不是根据他们对目标类型的了解(至少,我上次检查过)进行任何类型强制。 那么,你拒绝接受字符串吗? 您是否将所有内容都设为字符串,即使这会使您的类型难以使用? 或者你会做 TypeScript 会让你做的事情,并使用如下类型:string | 颜色,但使用吸气剂很糟糕。 这些都是非常糟糕的选项,会降低安全性,而类型系统的一些额外表现力会有所帮助。

问题是,导致这些问题的不仅仅是 Angular。 Angular 在这种情况下结束了,因为它反映了 DOM 中的许多场景,其中自动强制发生在属性集上,但 getter 始终是预期的单一类型。

对我来说,当您想围绕一些通用存储创建包装器时,通常会出现问题,它可以存储键值对并且您希望将用户限制为某些键。

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 。 目前,您不能这样做,因为 setter 输入类型必须包含null

@fan-tom,为什么需要重新打开此问题的绝佳用例! 让他们来。

这个问题的赞成票数非常高!

这是 TS 团队的项目,所以他们可以随心所欲地做他们认为合适的事情。 但是,如果他们的目标是让这对外部用户社区尽可能有用,那么我希望 TS 团队能够充分考虑社区的高票数。

typescriptlang.org 的主页目前在声明时不准确:

TypeScript 是 JavaScript 的类型化超集,可编译为纯 JavaScript。

TypeScript 是 JavaScript _subset_ 的类型化_superset_,可编译为纯 JavaScript。 :笑脸:

TypeScript 是 JavaScript 子集的类型化超集,可编译为纯 JavaScript。

更圆滑地说,我认为你可以说它是 JavaScript 的一个自以为是的超集,当团队从类型系统建模中做出这种自以为是的遗漏时。

他们说,“我们不会模仿这个,因为我们很难做到,而且我们认为无论如何你都不应该这样做”。 但是 JavaScript 塞满了你不应该做的事情,如果你有意愿和专业知识,通常 Typescript 不会阻止你做这些事情,因为似乎一般策略是不要对 JavaScript 什么固执己见你可以也不能只作为 Typescript 运行。

这就是为什么拒绝为这种常见的(在 DOM 中使用!)场景建模是如此奇怪,理由是认为 API 不应该执行基于 setter 的强制作为不这样做的理由。

目前,这似乎是不可能的,我不得不求助于这样的事情:

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) ,那么知道符号 foo 及其声明的PropertyAccessExpression可以有条件地检查是否存在“accessDelclaration”和改用那个打字。

假设存在将accessDelclaration属性添加到“foo”符号的语法,PropertyAccessExpression 应该能够从它从ts.createIdentifier("foo")获得的符号中提取“AccessSpecification”并产生不同的类型。

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: location = ' http://www.example.com ' 是 location.href = ' http://www.example.com ' 的同义词.
资源

将旧 JS 代码迁移到 TS,其中将string分配给window.location不起作用,我不得不做as any解决方法

这是一个很好的例子。

TS需要这个。 这是 JavaScript 的一个非常正常的部分。

我看到当前问题已关闭。 但据我了解,这个功能没有实现?

@AGluk ,这个问题需要重新打开。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

sandersn picture sandersn  ·  265评论

tenry92 picture tenry92  ·  146评论

blakeembrey picture blakeembrey  ·  171评论

RyanCavanaugh picture RyanCavanaugh  ·  205评论

nitzantomer picture nitzantomer  ·  135评论