Typescript: 允许用符号索引

创建于 2015-01-30  ·  93评论  ·  资料来源: microsoft/TypeScript

TypeScript现在具有ES6目标模式,其中包括定义Symbol 。 但是,当尝试使用符号索引对象时,出现错误(索引表达式参数的类型必须为'string','number'或'any')。

var theAnswer = Symbol('secret');
var obj = {};
obj[theAnswer] = 42; // Currently error, but should be allowed
Moderate Fix Available Suggestion help wanted

最有用的评论

打字稿3.0.1,被这个咬了。
我想要一个接受symbol的记录,但TS不允许我这样做。

自本期开刊已经3.5年了,请问我们现在可以使用符号吗?

具有讽刺意味的是,TS本身就是矛盾的。
TS展开keyof any = number | string | symbol

但是然后当您这样做record[symbol] TS拒绝说
_Type'symbol'不能用作索引器_。

所有93条评论

这是我们@JsonFreeman正在开发的ES6 Symbol支持的一部分。 您的代码示例将在下一版本中得到支持。

@wereHamster ,请求请求为1978,这应该合法,并且obj[theAnswer]类型any 。 这足以满足您的需求,还是您需要更强的打字能力?

是否可以指定由符号索引的属性类型? 类似于以下内容:

var theAnswer = Symbol('secret');
interface DeepThought {
   [theAnswer]: number;
}

根据该PR中的评论,否:

_这不涉及符号索引器,它允许对象使用任意符号键充当映射。

我认为@wereHamster在谈论一种比@danquirk更强的打字方式。 这里有3个支持级别。 我的PR提供了最基本的级别,但这仅适用于作为全局Symbol对象的属性的符号,而不是用户定义的符号。 所以,

var theAnswer = Symbol('secret');
interface DeepThought {
    [Symbol.toStringTag](): string; // Allowed
    [theAnswer]: number; // not allowed
}

下一个支持级别是允许使用符号索引器:

var theAnswer = Symbol('secret');
interface DeepThought {
   [s: symbol]: number;
}
var d: DeepThought;
d[theAnswer] = 42; // Typed as number

这是我们的雷达,可以轻松实现。

最强的要求是您所要求的,类似于:

var theAnswer = Symbol('secret');
var theQuestion = Symbol('secret');
interface DeepThought {
   [theQuestion]: string;
   [theAnswer]: number;
}
var d: DeepThought;
d[theQuesiton] = "why";
d[theAnswer] = 42;

这真的很好,但是到目前为止,我们还没有为它设计一个明智的设计。 最终似乎取决于使类型取决于这些符号的运行时值。 我们将继续考虑,因为这显然是一件有用的事情。

使用我的PR,您至少应该能够使用符号来提取对象的值_out_。 它将是any ,但是您将不再收到错误。

@wereHamster我写了一些#2012,您可能对此感兴趣。

我已经合并了1978年的请求,但是我将保持此bug的打开状态,因为它似乎要求的不只是我提供的更改。 但是,通过我的更改,原始错误将消失。

@wereHamster您可以在这里发布您想看到的更多内容的更新吗? 现在还不清楚我已经实施了什么与您发布了什么

知道何时symbol将成为索引器的有效类型吗? 作为社区PR可以做到这一点吗?

我们会为此做一个公关。 @JsonFreeman可以提供您可能遇到的一些问题的详细信息。

我实际上认为添加符号索引器非常简单。 它会像数字和字符串一样工作,不同之处在于它在可分配性,类型参数推断等方面都不兼容。主要的挑战是确保记住在所有适当的位置添加逻辑。

@RyanCavanaugh ,最终在https://github.com/Microsoft/TypeScript/issues/1863#issuecomment -73668456 typecheck中找到最后一个示例将是很好的。 但是,如果您愿意,可以将此问题分解为多个较小的问题,它们相互叠加。

在这方面是否有任何更新? AFAIU的最新版本的编译器仅支持https://github.com/Microsoft/TypeScript/issues/1863#issuecomment -73668456中描述的第一级。

我们很乐意接受此更改的PR。

可能需要将两个级别作为两个独立的问题进行跟踪。 索引器看似相当简单,但实用程序尚不清楚。 持续跟踪的全面支持似乎相当困难,但可能更有用。

知道了,这很有意义。

@JsonFreeman @ mhegazy #12932有问题

只是以为我会把用例扔进去。 我正在编写一个工具,该工具允许通过指定用于与任意对象属性进行匹配的纯文本键以及用于指定匹配运算符的符号来描述查询。 通过将符号用于知名运算符,可以避免将运算符与键与知名运算符的字段匹配的模棱两可。

由于不能将符号指定为索引键,因此与JavaScript明确允许的相反,我不得不在多个地方强制转换为<any> ,这会降低代码质量。

interface Query {
  [key: string|symbol]: any;
}

const Q = {
  startsWith: Symbol('startsWith'),
  gte: Symbol('gte'),
  lte: Symbol('lte')
}

const sample: Query = {
  name: {
    [Q.startsWith]: 'M',
    length: {
      [Q.lte]: 25
    }
  },
  age: {
    [Q.gte]: 18
  }
};

鉴于查询引擎可能需要检查的数据种类繁多,因此使用“不太可能”的第一个字符(例如$字符)不是适当的折衷方案。

嗨,大家好。 这有什么动静吗? 我需要它,所以我很乐意做出必要的更改。 以前没有对TS做出过贡献。

@mhegazy @RyanCavanaugh我知道你们都很忙,但是有机会时您可以考虑一下吗? 符号是构建库和框架的真正重要工具,缺乏在接口中使用它们的能力无疑是一个痛点。

我问有什么进展吗? 衷心希望此功能得到支持。

是的,今天仍然在寻找这个,这是我在Webstorm中看到的:

screenshot 2017-10-08 21 37 17

确实有效

var test: symbol = Symbol();

const x = {
    [test]: 1
};

x[test];

console.log(x[test]);

console.log(x['test']);

但是x类型不正确,被推断为

{
  [key: string]: number
}

是的,今天仍然在寻找这个,这是我在Webstorm中看到的:

请注意,JetBrains自己的语言服务已在WebStorm,智能IDEA等默认情况下启用。

这在TS 2.7中有效

const key = Symbol('key')
const a: { [key]?: number } = {}
a[key] = 5

这事有进一步更新吗?

我的问题:

export interface Dict<T> {
  [index: string]: T;

  [index: number]: T;
}

const keyMap: Dict<number> = {};

function set<T extends object>(index: keyof T) {
  keyMap[index] = 1; // Error Type 'keyof T' cannot be used to index type 'Dict<number>'
}

但这也不起作用,因为符号不能是索引类型。

export interface Dict<T> {
  [index: string]: T;
  [index: symbol]: T; // Error: An index signature parameter type must be 'string' or 'number'
  [index: number]: T;
}

预期行为:
symbol应该是有效的索引类型

实际行为:
symbol不是有效的索引类型

在投射as string | number使用变通方法对我来说似乎很糟糕。

应该如何在TypeScript中使用util.promisify.custom ? 似乎现在支持使用常量符号,但前提是必须明确定义它们。 因此,这是有效的TypeScript(除了未初始化f ):
typescript const custom = Symbol() interface PromisifyCustom<T, TResult> extends Function { [custom](param: T): Promise<TResult> } const f: PromisifyCustom<string, void> f[custom] = str => Promise.resolve()
但是,如果使用promisify.custom而不是custom ,则尝试引用f[promisify.custom]导致错误Element implicitly has an 'any' type because type 'PromisifyCustom<string, void>' has no index signature.
typescript import {promisify} from 'util' interface PromisifyCustom<T, TResult> extends Function { [promisify.custom](param: T): Promise<TResult> } const f: PromisifyCustom<string, void> f[promisify.custom] = str => Promise.resolve()
我想分配给函数的promisify.custom字段,但是(鉴于上述行为)似乎唯一的方法是将函数强制转换为any类型。

我不明白为什么不允许符号作为键索引,下面的代码应该可以工作并且被Typescript 2.8接受,但是却被Typescript 2.9接受

/**
 * Key can only be number, string or symbol
 * */
export class SimpleMapMap<K extends PropertyKey, V> {
  private o: { [k: string ]: V } = {};

  public has (k: K): boolean {
    return k in this.o;
  }

  public get (k: K): V {
    return this.o[k as PropertyKey];
  }

  public set (k: K, v: V) {
    this.o[k as PropertyKey] = v;
  }

  public getMap (k: K): V {
    if (k in this.o) {
      return this.o[k as PropertyKey];
    }
    const res = new SimpleMapMap<K, V>();
    this.o[k as PropertyKey] = res as any as V;
    return res as any as V;
  }

  public clear () {
    this.o = {};
  }
}

我在下面尝试过,这对我来说更“正确”,但两个版本的Typescript编译器均不接受

/**
 * Key can only be number, string or symbol
 * */
export class SimpleMapMap<K extends PropertyKey, V> {
  private o: { [k: K ]: V } = {};

  public has (k: K): boolean {
    return k in this.o;
  }

  public get (k: K): V {
    return this.o[k];
  }

  public set (k: K, v: V) {
    this.o[k] = v;
  }

  public getMap (k: K): V {
    if (k in this.o) {
      return this.o[k];
    }
    const res = new SimpleMapMap<K, V>();
    this.o[k as PropertyKey] = res as any as V;
    return res as any as V;
  }

  public clear () {
    this.o = {};
  }
}

该票证的状态指示您所建议的是所需的行为,但是核心团队此时尚未投入资源来添加此功能增强,而是将其开放给社区以解决。

@beenotung尽管这不是理想的解决方案,但假设您发布的类是唯一需要这种行为的地方,则可以在类内部进行不安全的强制类型转换,但要使类和方法签名保持相同,从而使使用者的班级不会看到:

/**
 * Key can only be number, string or symbol
 * */
export class SimpleMapMap<K extends PropertyKey, V> {
  private o: { [k: string]: V } = {};

  public has(k: K): boolean {
    return k in this.o;
  }

  public get(k: K): V {
    return this.o[k as any];
  }

  public set(k: K, v: V) {
    this.o[k as any] = v;
  }

  public getMap(k: K): V {
    if (k in this.o) {
    return this.o[k as any];
    }

    const res = new SimpleMapMap<K, V>();
    this.o[k as any] = res as any as V;
    return res as any as V;
  }

  public clear() {
    this.o = {};
  }
}

因为签名是相同的,所以每当您使用此类时,都将正确应用类型验证,并且,在解决此问题后,您只需更改此类(对使用者透明)。

使用者的示例就像下面这样(解决此问题后,无需更改任何代码):

const s1 = Symbol(1);
const s2 = Symbol(2);

let m = new SimpleMapMap<symbol, number>()
m.set(s1, 1);
m.set(s2, 2);
m.get(s1);
m.get(1); //error

打字稿3.0.1,被这个咬了。
我想要一个接受symbol的记录,但TS不允许我这样做。

自本期开刊已经3.5年了,请问我们现在可以使用符号吗?

具有讽刺意味的是,TS本身就是矛盾的。
TS展开keyof any = number | string | symbol

但是然后当您这样做record[symbol] TS拒绝说
_Type'symbol'不能用作索引器_。

是的,我已经对此感到痛苦了一段时间,这是我关于此主题的最新问题:

https://stackoverflow.com/questions/53404675/ts2538-type-unique-symbol-cannot-be-used-as-an-index-type

@RyanCavanaugh @DanielRosenwasser @mhegazy有任何更新吗? 这个问题已接近其第四个生日。

如果有人可以指出正确的方向,我就可以解决。 如果有匹配的测试,那就更好了。

@jhpratt在# 在此保留PR的更多反馈。 它似乎被视为边缘问题/影响较小的问题,因此对PR进行更多投票可能有助于提高问题的知名度。

谢谢@yortus。 我刚刚问过赖安(Ryan),是否仍计划将PR计划为3.2,这是里程碑式的说明。 希望是这样,并且可以解决!

@yortus指出的PR似乎是一个巨大的变化,
这个错误的解决方法应该很小吗? 例如在条件检查中添加或语句。
(我尚未找到要更改的地方。)

这里的临时解决方案https://github.com/Microsoft/TypeScript/issues/24587#issuecomment -412287117,有点丑陋,但可以完成工作

const DEFAULT_LEVEL: string = Symbol("__default__") as any;

另一个https://github.com/Microsoft/TypeScript/issues/24587#issuecomment -460650063,因为linters h8 any

const ItemId: string = Symbol('Item.Id') as unknown as string;
type Item = Record<string, string>;
const shoes: Item = {
  name: 'whatever',
}
shoes[ItemId] = 'randomlygeneratedstring'; // no error
{ name: 'whatever', [Symbol(Item.Id)]: 'randomlygeneratedstring' }

我猜我在使用符号时注意到的陷阱之一是,如果您有一个涉及child_process模块​​的项目,是的,您可以在两个进程之间共享类型/枚举/接口,但不能共享符号。

解决这个问题确实很棒,符号确实可以很好地跟踪对象而不会污染其键,也不需要使用地图/集合,此外,近年来的基准测试表明,访问符号与访问字符串一样快/数字键


编辑:原来这种方法仅适用于Record<X,Y> ,而不适用于Interfaces 。 最终使用// @ts-ignore ,因为它在语法上仍然是正确的,并且仍然可以很好地编译为JS。

不过要注意的是,在涉及符号的行上使用// @ts-ignore时,实际上有可能(并有帮助)手动指定该符号的类型。 VSCode仍然对此有所了解。

const id = Symbol('ID');

interface User {
  name: string;
  age: number;
}

const alice: User = {
  name: 'alice',
  age: 25,
};

// @ts-ignore
alice[id] = 'maybeSomeUUIDv4String';

// ...

// then somewhere, when you need this User's id

// @ts-ignore
const id: string = alice[id];

console.log(id); // here you can hover on id and it will say it's a string

不知道,是否有人开始解决此问题,但如果没有,我现在有了。

我的时间有限,但是我对Typescript来源的知识为零。 我做了一个fork(https://github.com/Neonit/TypeScript),但是还没有请求请求,因为我不想用未完成(?)的更改来破坏开发人员。 我想请大家为我的叉子做些什么。 我最终将发布PR。

到目前为止,我已经找到一种解决接口索引类型限制的方法。 我不知道,是否还有更多内容。 我能够在TS 3.4中使用符号索引对象,而无需进行任何修复。 ( https://www.typescriptlang.org/play/#src = const%20o%20%3D%20%7B%7D%3B%0D%0Aconst%20s%20%3D%20Symbol('s')%3B %0D%0A%0D%0Ao%5Bs%5D%20%3D%20123%3B)

看看我的提交,看看我发现了什么: https :

失踪:

  • 测试:我没有时间研究TS中测试的组织和结构。
  • 本地化:仅针对英语版本更新了诊断消息。 也许其他语言仍旧传达了旧信息。 我不知道。 无论如何,我只能提供德语翻译。

我希望,经过多年的等待,这将最终使事情开始。

修复看起来不错。 TypeScript开发人员可以看看吗?

你好,这有什么进展吗?

刚刚打开了一个关于此的SO线程: https :

为什么不可能呢? symbol不是另一个原始类型,例如number -那么为什么会有区别呢?

您好,这有什么进展吗?

五年已经过去了!

您不会相信C ++需要多长时间才能获得闭包😲

大声笑,但C ++并未将自身营销为具有闭包的语言的超集:-p

@ljharb继续殴打那匹马,它还在抽搐😛

对于那些针对较新运行时的用户,为什么不使用Map呢? 我奇怪地发现,很多开发人员都不知道Map的存在,所以我很好奇是否还有另外一种情况。

let m = new Map<symbol, number>();
let s = Symbol("arbitrary symbol!");

m.set(s, 1000);
let a = m.get(s);


地图和对象具有不同的用例。

@DanielRosenwasser众所周知的符号用作协议; 例如, Symbol.match的Map键不会使对象类似于RegExp(并且任何对象都可能希望Symbol.iterable键使其可迭代,而不必显式使用内置的TS迭代类型)。

近5年(

请实现此功能,我不能正常编写代码。

参与者可以在他们的用例中提供实际示例吗?

我不了解协议示例以及为什么今天无法实现。

这是StringConvertible的示例

const intoString = Symbol("intoString")

/**
 * Something that can be converted into a string.
 */
interface StringConvertible {
    [intoString](): string;
}

/**
 * Something that is adorable.
 */
class Dog implements StringConvertible {
    [intoString](): string {
        return "RUFF RUFF";
    }
}

/**
 * <strong i="9">@see</strong> {https://twitter.com/drosenwasser/status/1102337805336768513}
 */
class FontDog implements StringConvertible {
    [intoString](): string {
        return "WOFF WOFF";
    }
}

console.log(new Dog()[intoString]())
console.log(new FontDog()[intoString]())

这是MappableFunctor (缺少高阶类型构造函数)的示例:

const map = Symbol("map")

interface Mappable<T> {
    [map]<U>(f: (x: T) => U): Mappable<U>
}

class MyCoolArray<T> extends Array<T> implements Mappable<T> {
    [map]<U>(f: (x: T) => U) {
        return this.map(f) as MyCoolArray<U>;
    }
}

@DanielRosenwasser好像您假设所有对象都有一个接口,或者是一个类实例,或者是事先已知的; 使用您的上一个示例,我应该能够将map安装到任何javascript对象上(或至少将其类型允许添加任何符号的对象安装到

事实结束后将属性(是否带符号)安装到对象上是另一个功能请求(通常称为“ expando属性”或“ expando类型”)的一部分。

缺少这一点,符号索引签名所需的类型将不会像TypeScript用户那样提供足够的信息,对吗? 如果我理解正确,类型将需要像unknown或只是any的东西才有用。

interface SymbolIndexable {
   [prop: symbol]: any; // ?
}

就协议而言,它通常是一个函数,但可以肯定的是,它可能是unknown

我需要的是等价于type O = { [k: string]: unknown }的符号(和bigint),因此我可以用类型系统表示一个实际的JS对象(可以具有任何类型的键的对象)。 我可以根据需要稍后缩小范围,但是JS对象的基本类型实际上是{ [k: string | bigint | symbol | number]: unknown }

嗯,我想我看到了@DanielRosenwasser点。 我目前有接口类似的代码:

export interface Environment<T> {
    [Default](tag: string): Intrinsic<T>;
    [Text]?(text: string): string;
    [tag: string]: Intrinsic<T>;
    // TODO: allow symbol index parameters when typescript gets its shit together
    // [tag: symbol]: Intrinsic<T>;
}

其中Intrinsic<T>是函数类型,我希望允许开发人员在类似于字符串的环境中定义自己的符号属性,但是在可以添加[Symbol.iterator][Symbol.species]或自定义符号属性应用于任何接口,带有符号的索引签名会错误地限制实现这些属性的任何对象。

所以,您要说的是,不能使符号索引的值类型比any更具体吗? 我们可以以某种方式使用unique symbolsymbol区别来允许吗? 就像我们可以将索引签名作为常规符号的默认设置,并允许唯一/众所周知的符号覆盖索引类型一样吗? 即使不是类型安全的,能够通过符号索引任意获取/设置属性也会很有帮助。

另一种选择是让用户使用其符号属性自己扩展环境接口,但这不会提供任何附加的类型安全性,因为用户可以在对象上键入Symbol。

@DanielRosenwasser这里是我的生产代码的真实示例。 一个在许多地方作为地图重用的国家,可以接受原子键(域特征)。 当前,我需要添加符号支持,但是出现很多错误:


无论如何,当前行为与错误的ES标准不兼容。

还有一个深夜的想法让我想到了符号类型。 为什么这不是错误?

const foo = {
  [Symbol.iterator]: 1,
}

JS希望所有Symbol.iterator属性都是一个返回迭代器的函数,如果该对象在各个地方传递,则它将破坏很多代码。 如果有一种方法可以全局定义所有对象的符号属性,那么我们可以允许使用特定的符号索引签名,同时还可以使用全局替代。 它将是类型安全的,对吗?

我也不明白为什么在这里需要用例。 这是ES6不兼容,在包装ES6的语言中不应该存在。

过去,我在此线程中如何解决此问题发表了

我只是没有发出请求,因为我不了解Typescript的测试框架或要求,也因为我不知道是否需要在不同文件中进行更改才能在所有情况下都可以进行这项工作。

因此,在继续花时间在这里阅读和写作之前,请检查添加该功能是否会减少耗时。 我怀疑有人会抱怨它在Typescript中。

除了所有一般用例之外,您还想将值映射到任意符号上。 或与非类型化ES6代码兼容。

这是一个我认为会有所帮助的地方的示例: https : eventName s可以是符号或字符串,所以我想说

type EventsConfiguration = { [eventName: string | Symbol]: (...args: any[]) => void }

在第一行。

但是我可能会误解我应该如何做。

简单的用例就不可能没有痛苦:

type Dict<T> = {
    [key in PropertyKey]: T;
};

function dict<T>() {
    return Object.create(null) as Dict<T>;
}

const has: <T>(dict: Dict<T>, key: PropertyKey) => boolean = Function.prototype.call.bind(Object.prototype.hasOwnProperty);

function forEach<T>(dict: Dict<T>, callbackfn: (value: T, key: string | symbol, dict: Dict<T>) => void, thisArg?: any) {
    for (const key in dict)
        if (has(dict, key))
            callbackfn.call(thisArg, dict[key], key, dict);
    const symbols = Object.getOwnPropertySymbols(dict);
    for (let i = 0; i < symbols.length; i++) {
        const sym = symbols[i];
        callbackfn.call(thisArg, dict[sym], sym, dict); // err
    }
}

const d = dict<boolean>();
const sym = Symbol('sym');
const bi = 9007199254740991n;

d[1] = true;
d['x'] = true;
d[sym] = false; // definitely PITA
d[bi] = false; // another PITA

forEach(d, (value, key) => console.log(key, value));

我也不明白为什么在这里需要用例。

@neonit有PR可以解决这个问题,但是我的理解是,功能与其他类型系统的交互方式存在细微的问题。 缺乏解决方案,我要求用例的原因是因为我们不能立即工作/专注于我们想要的每个功能-因此用例需要证明完成的工作是合理的,包括长期维护功能。

看来,事实上,大多数人想象中的用例不会像他们想象的那样容易解决(请参阅@brainkim在这里的响应https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574550587),或通过符号属性(https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574538121)或地图(https://github.com/microsoft/TypeScript/issues/1863)都能很好地解决它们#issuecomment-572733050)。

我认为@ Tyler-Murphy在这里提供了最好的示例,因为您无法编写约束,这对于支持符号的类型安全事件发射器之类的东西可能非常有用。

因此,在继续花时间在这里阅读和写作之前,请检查添加该功能是否会减少耗时。 我怀疑有人会抱怨它在Typescript中。

当您不必维护项目时,这总是很容易说出来! 😄我知道这对您很有用,但我希望您尊重。

这是ES6的不兼容

TypeScript不能轻易键入很多构造,因为这是不可行的。 我并不是说这是不可能的,但我不认为这是解决此问题的合适方法。

因此,似乎无法添加符号键作为索引签名是由于以下事实:存在着全球知名的Symbols,它们需要自己键入,而这些符号索引类型将不可避免地与之发生冲突。 作为解决方案,如果我们有一个表示所有已知符号的全局模块/接口怎么办?

const Answerable = Symbol.for("Answerable");
declare global {
  interface KnownSymbols {
    [Answerable](): string  | number;
  }
}

interface MyObject {
  [name: symbol]: boolean;
}

const MySymbol = Symbol.for("MySymbol");
const obj: MyObject = {
  [MySymbol]: true,
};

obj[Answerable] = () => "42";

通过在全局KnownSymbols接口上声明其他属性,可以允许该符号对所有对象建立索引,并将属性的值限制为未定义/您的值类型。 通过允许打字稿为ES6提供的众所周知的符号提供打字,这将立即提供价值。 将Symbol.iterator属性添加到不是返回迭代器的函数的对象显然应该是错误,但当前不是打字稿中的错误。 它将使向现有对象中添加众所周知的符号属性变得更加容易。

全局模块的这种用法也将允许将Symbols用作任意键,因此也可以用作索引签名。 您只需赋予全局已知符号属性优先于本地索引签名类型。

实施此建议是否可以使索引签名类型向前发展?

各个用例无关紧要。 如果是复杂的JavaScript,则需要在TS定义中表达。

但我的理解是,功能与类型系统其余部分的交互方式存在细微问题

确切地说,更像是“重构索引签名如何在内部完全起作用,这是一个可怕的大变化,并引发了关于索引签名与不使用模板变量的映射类型有何不同或应该与之不同的问题”。

它主要导致了关于我们如何无法识别封闭类型与开放类型的讨论。 在这种情况下,“封闭”类型将是具有一组有限键的类型,其值不能扩展。 如果可以的话,可以使用一种精确类型的键。 同时,在这种情况下,“开放”类型是一种类型,当被子类型化时,它是开放的以添加更多的键(在我们当前的子类型规则下,sorta所有类型有时大多数都是除外,除了带有索引签名的类型非常明确)。 。 索引签名意味着建立开放类型,而映射类型则在很大程度上与封闭类型有关。 通常来说,这很有效,因为实际上大多数代码都是使用与封闭对象类型兼容的结构编写的。 这就是为什么flow (对于关闭的对象类型和打开的对象类型具有显式语法)默认为关闭的对象类型的原因。 这是通用索引键的开头。 如果我有T extends string ,因为T实例化的类型越来越广泛(从"a""a" | "b"string ),则对象直到我们从"a" | "b" | ... (every other possible string)交换到string本身为止,生产的产品越来越专业。 一旦发生这种情况,该类型就会突然变得非常开放,并且虽然每个属性都可能存在以供访问,但是例如为其分配一个空对象是合法的。 从结构上讲就是这样,但是当我们在映射类型中关联泛型时,我们忽略了这一点-泛型映射类型键上的string约束本质上是相关的,就好像它使所有可能的键都存在一样。 从逻辑上讲,这是基于键类型的简单的基于方差的视图,但是仅当键来自_closed_类型(ofc,带有索引签名的类型从不真正关闭!)时才是正确的。 因此,如果我们想向后兼容,我们就不能将{[x: T]: U}{[_ in T]: U} ,除非我们想这样做,因为在非一般情况下{[_ in T]: U}变成{[x: T]: U} ,调整我们如何处理映射的类型键的方差,以适当地考虑开放类型“ edge”,这本身就是一个有趣的变化,可能会对生态系统产生影响。

几乎是:因为它使映射的类型和索引签名更紧密地联系在一起,所以引发了一系列有关如何处理这两种类型的问题,这些问题我们尚无满意或结论性的答案。

各个用例无关紧要。

礼貌地说,这是纯粹的疯狂。 我们如何才能知道我们是否在不使用用例来判断行为的情况下添加人们想要的行为的功能?

我们不问这些问题就在这里努力。 我们实际上是在努力确保我们实现人们所要求的东西。 如果我们实现了我们认为“用符号建立索引”的东西,只是让这个线程中的同一个人回来并说我们做错了完全是错误的,因为它没有解决他们的特定用例,那真是太可惜了。

您是在要求我们盲目飞行。 请不要! 请告诉我们您想乘飞机去哪里!

不好,我本来可以清楚我的意思; 在我看来,人们觉得他们不得不证明自己的实际用例,而不是希望通过TS进行更准确的描述

因此,如果我正确理解,则主要是关于以下问题:

const sym = Symbol();
interface Foo
{
    [sym]: number;
    [s: symbol]: string; // just imagine this would be allowed
}

现在,Typescript编译器会将其视为冲突,因为Foo[sym]具有矛盾的类型。 字符串已经存在相同的问题。

interface Foo
{
    ['str']: number; // <-- compiler error: not assignable to string index type 'string'
    [s: string]: string;
}

字符串索引的处理方式是,如果存在通用的字符串键规范且其类型不兼容,则不允许使用特定的字符串索引。

对于符号来说,这将是一个无所不在的问题,因为ECMA2015定义了标准符号,例如Symbol.iterator ,它可以在任何对象上使用,因此应该具有默认类型。 他们奇怪地显然没有。 至少操场上不允许我运行MDN

假设计划添加预定义的符号类型,则总会导致通用的[s: symbol]: SomeType定义无效,因为预定义的符号索引已经具有不兼容的类型,因此不能存在通用的通用类型,或者可能需要是function类型,因为大多数(/全部?)预定义符号键都是function类型。

通用索引类型和特定索引类型混合的问题是,使用在编译时未知的值对对象进行索引时的类型确定。 想象一下我上面的带有字符串索引的示例将是有效的,那么下面的操作将是可能的:

const foo: Foo = {str: 42, a: 'one', b: 'two'};
const input: string = getUserInput();
const value = foo[input];

同样的问题也适用于符号键。 在编译时无法确定value的确切类型。 如果用户输入'str' ,它将是number ,否则它将是string (至少Typescript希望它是string ,而它是可能会变成undefined )。 这是我们没有此功能的原因吗? 可以通过为value一个包含定义中所有可能类型的联合类型来解决此问题(在本例中number | string )。

@Neonit好吧,这不是

如果此功能实现,ECMAScript中内置的符号不一定会毁了一切,因为不是每个类型使用这些符号; 但任何确实使用众所周知的符号(或您自己定义的符号)定义属性的类型都可能仅限于符号使用较少的索引签名。

这实际上是要记住的事情-从类型系统的角度来看,“我想将其用作地图”和“我想使用符号来实现协议”用例是不兼容的。 因此,如果您有类似的想法,那么符号索引签名可能无济于事,并且可以通过显式的符号属性或映射为您提供更好的服务。

UserSymbol类型只是symbol减去内置符号的东西呢? 符号的本质确保了不会发生意外碰撞。

编辑:仔细考虑一下,众所周知的符号只是使用Symbol实现的标记。 除非目标是对象序列化或自省,否则代码可能应该将这些标记与其他符号区别对待,因为它们对语言具有特殊含义。 从symbol类型中删除它们可能会使使用“通用”符号的(大多数)代码安全。

@RyanCavanaugh这是我的飞行计划。

我有一个系统,其中将这样的符号用作属性。

const X = Symbol.for(":ns/name")

const txMap = {
  [X]: "fly away with me!"
}

transact(txMap) // what's the index signature here?

在这种情况下,我希望txMap适合transact的类型签名。 但是据我所知,我今天无法表达这一点。 就我而言, transact是不知道会发生什么的库的一部分。 我为属性做这样的事情。

// please forgive my tardiness but in essence this is how I'm typing "TxMap" for objects
type TxMapNs = { [ns: string]: TxMapLocal }
type TxMapLocal = { [name: string]: string | TxMapNs } // leaf or non leaf

我可以从架构中生成适合transact的类型集并使用它。 为此,我会做这样的事情,并依赖于声明合并。

interface TxMap = {
  [DB_IDENT]: symbol // leaf
  [DB_VALUE_TYPE]?: TxMap // not leaf
  [DB_CARDINALITY]?: TxMap
}

但是,如果我至少可以回退到符号的索引签名,那太好了,我只希望将transact传递给普通的JavaScript对象,在这种情况下,我也只使用全局符号注册表中的符号。 我不使用私人符号。


我还要补充一点,这有点麻烦。

const x = Symbol.for(":x");
const y = Symbol.for(":x");

type X = { [x]: string };
type Y = { [y]: string };

const a: X = { [x]: "foo" };
const b: Y = { [x]: "foo" }; // not legal
const c: X = { [y]: "foo" }; // not legal
const d: Y = { [y]: "foo" };

如果TypeScript可以理解通过Symbol.for函数创建的符号实际上是相同的,那就太棒了。


这也超级烦人。

function keyword(ns: string, name: string): unique symbol { // not possible, why?
  return Symbol.for(":" + ns + "/" + name)
}

const x: unique symbol = keyword("db", "id") // not possible, why?

type X = {
  [x]: string // not possible, why?
}

这个小工具功能让我在全局符号表上强制执行约定。 但是,即使它是通过Symbol.for函数创建的,我也无法返回unique symbol 。 由于TypeScript的处理方式,它迫使我放弃某些解决方案。 他们只是不工作。 我认为这很可悲。

我遇到了另一个用例,其中在与ES Proxies一起创建使用symbol作为索引值将很有用。

举个例子:

let original = {
    foo: 'a',
    bar: 'b',
    baz: 1
};

function makeProxy<T extends Object>(source: T) {
    return new Proxy(source, {
        get: function (target, prop, receiver) {
            return target[prop];
        }
    });
}

let proxied = makeProxy(original);

要匹配ProxyConstructor类型签名,泛型参数必须扩展Object ,但这会出错,因为泛型参数未输入键。 因此,我们可以扩展类型签名:

function makeProxy<T extends Object & { [key: string]: any}>(source: T) {

但现在,因为第二个参数(它会引发错误prop )的getProxyHandler的类型为PropertyKey这恰巧是PropertyKey

因此,由于此问题的限制,我不确定如何使用TypeScript进行此操作。

@aaronpowell您正在面对什么问题? 我看到它表现良好:

let original = {
    foo: 'a',
    bar: 'b',
    baz: 1
};

function makeProxy<T extends Object>(source: T) {
    return new Proxy(source, {
        get: function (target, prop, receiver) {
            return target[prop];
        }
    });
}

let proxied = makeProxy(original);

function assertString(s:string){}
function assertNumber(x:number){}

assertString(proxied.foo); // no problem as string
assertNumber(proxied.baz); // no problem as number
console.log(proxied.foobar); // fails as expected: error TS2339: Property 'foobar' does not exist on type '{ foo: string; bar: string; baz: number; }'.

tsconfig.json:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "node",
    "target": "es2015"
  }

package.json:

{
  "devDependencies": {
    "typescript": "~3.4.5"
  }
}

@beenotung我在操场上看到一个错误:

image

@aaronpowelltsconfig.json的'compilerOptions'中启用'strict'标志时出现错误。

因此,在当前版本的Typescript编译器下,您必须关闭严格模式或将目标强制转换为any ...

可以,但是any强制转换并不是真正理想的选择,禁用严格模式只是放松类型安全性的限制。

阅读消息,我想下一个“解决方案”可能是“禁用打字稿”。

我们不必搜索权宜之计的解决方案,也不必解释我们为什么需要它。

这是javascript的标准功能,因此我们需要在打字稿中使用它。

@DanielRosenwasser我的使用情况是类似的@aaronpowell的-在一个表面上的不匹配ProxyHandler接口和打字稿的规则阻止我正确输入代理处理程序陷阱。

一个简单的例子说明了这个问题:

const getValue = (target: object, prop: PropertyKey) => target[prop]; // Error

据我所知,不可能为target任何类型来避免错误,但只允许PropertyKey合法访问的对象。

我是TypeScript新手,所以如果我缺少明显的内容,请原谅我。

另一个用例:我正在尝试为调用者提供类型{[tag: symbol]: SomeSpecificType} ,以提供一种特定类型的标记值的映射,从而从对象文字语法的紧凑性中受益(同时仍避免名称冲突)使用纯字符串作为标签的风险)。

另一个用例:我正在尝试迭代对象,符号和字符串的所有可枚举属性。 我当前的代码如下所示(名称被遮盖):

type ContextKeyMap = Record<PropertyKey, ContextKeyValue>

function setFromObject(context: Context, object: ContextKeyMap) {
    for (const key in object) {
        if (hasOwn.call(object, key)) context.setKey(key, object[key])
    }

    for (const symbol of Object.getOwnPropertySymbols(object)) {
        if (propertyIsEnumerable.call(object, symbol)) {
            context.setKey(symbol, object[symbol as unknown as string])
        }
    }
}

非常希望能够做到这一点:

type ContextKeyMap = Record<PropertyKey, ContextKeyValue>

function setFromObject(context: Context, object: ContextKeyMap) {
    for (const key in object) {
        if (hasOwn.call(object, key)) context.setKey(key, object[key])
    }

    for (const symbol of Object.getOwnPropertySymbols(object)) {
        if (propertyIsEnumerable.call(object, symbol)) {
            context.setKey(symbol, object[symbol])
        }
    }
}

我也有符号索引的问题。 我的代码如下:

const cacheProp = Symbol.for('[memoize]')

function ensureCache<T extends any>(target: T, reset = false): { [key in keyof T]?: Map<any, any> } {
  if (reset || !target[cacheProp]) {
    Object.defineProperty(target, cacheProp, {
      value: Object.create(null),
      configurable: true,
    })
  }
  return target[cacheProp]
}

我通过@aaronpowell跟踪解决方案,并设法解决了该问题

const cacheProp = Symbol.for('[memoize]') as any

function ensureCache<T extends Object & { [key: string]: any}>(target: T, reset = false): { [key in keyof T]?: Map<any, any> } {
  if (reset || !target[cacheProp]) {
    Object.defineProperty(target, cacheProp, {
      value: Object.create(null),
      configurable: true,
    })
  }

  return target[cacheProp]
}

symbol投射到any确实不是很好。

非常感谢其他解决方案。

@ahnpnl对于该用例,最好使用WeakMap比使用符号更好,并且引擎会更好地对其进行优化-它不会修改target的类型映射。 您可能仍然需要强制转换,但是强制转换将保留在返回值中。

一种解决方法是使用泛型函数分配值...

var theAnswer: symbol = Symbol('secret');
var obj = {} as Record<symbol, number>;
obj[theAnswer] = 42; // Currently error, but should be allowed

Object.assign(obj, {theAnswer: 42}) // allowed

一种解决方法是使用泛型函数分配值...

var theAnswer: symbol = Symbol('secret');
var obj = {} as Record<symbol, number>;
obj[theAnswer] = 42; // Currently error, but should be allowed

Object.assign(obj, {theAnswer: 42}) // allowed

我不同意。 这三行彼此相等:

Object.assign(obj, {theAnswer: 42});
Object.assign(obj, {'theAnswer': 42});
obj['theAnswer'] = 42;

@DanielRosenwasser
我有这个用例,在游乐场链接中,我也通过使用地图解决了它,但是看一下它的丑陋之处。

const system = Symbol('system');
const SomeSytePlugin = Symbol('SomeSytePlugin')

/** I would prefer to have this working in TS */
interface Plugs {
    [key: symbol]: (...args: any) => unknown;
}
const plugins = {
    "user": {} as Plugs,
    [system]: {} as Plugs
}
plugins[system][SomeSytePlugin] = () => console.log('awsome')
plugins[system][SomeSytePlugin](); ....

游乐场链接

在这里使用符号可以排除使用字符串时可能发生的意外覆盖。 它使整个系统更加健壮且易于维护。

如果您有可与TS一起使用且代码具有相同可读性的替代解决方案,我将不胜枚举。

有官员解释这个问题吗?

一种解决方法是使用泛型函数分配值...

var theAnswer: symbol = Symbol('secret');
var obj = {} as Record<symbol, number>;
obj[theAnswer] = 42; // Currently error, but should be allowed

Object.assign(obj, {theAnswer: 42}) // allowed

您正在寻找

Objet.assign(obj, { [theAnswer]: 42 });

但是,如果不强制转换AFAIK,则无法读取x[theAnswer]请参阅下面的注释2

为了上帝的爱,请优先考虑。

您正在寻找

Objet.assign(obj, { [theAnswer]: 42 });

但是,如果不强制转换AFAIK,则无法读取x[theAnswer]

正如mellonis和MingweiSamuel指出的那样,使用泛型函数的解决方法是:

var theAnswer: symbol = Symbol("secret");
var obj = {} as Record<symbol, number>;

obj[theAnswer] = 42; // Not allowed, but should be allowed

Object.assign(obj, { [theAnswer]: 42 }); // allowed

function get<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

var value = obj[theAnswer]; // Not allowed, but should be allowed

var value = get(obj, theAnswer); // allowed

五年,仍然不允许使用Symbol作为索引

在这种情况下找到了解决方法,它不是通用的,但在某些情况下可以工作:

const SYMKEY = Symbol.for('my-key');

interface MyObject {   // Original object interface
  key: string
}

interface MyObjectExtended extends MyObject {
  [SYMKEY]?: string
}

const myObj: MyObject = {
  'key': 'value'
}

// myObj[SYMKEY] = '???' // Not allowed

function getValue(obj: MyObjectExtended, key: keyof MyObjectExtended): any {
  return obj[key];
}

function setValue(obj: MyObjectExtended, key: keyof MyObjectExtended, value: any): void {
  obj[key] = value
}

setValue(myObj, SYMKEY, 'Hello world');
console.log(getValue(myObj, SYMKEY));

@ james4388您的示例与@beenotung的示例有何不同?

仅供参考: https :

(刚发现-我实际上不是TS团队的一员。)

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