Typescript: Permitir indexação com símbolos

Criado em 30 jan. 2015  ·  93Comentários  ·  Fonte: microsoft/TypeScript

O TypeScript agora tem um modo de destino ES6 que inclui as definições Symbol . No entanto, ao tentar indexar um objeto com um símbolo, recebo um erro (um argumento de expressão de índice deve ser do tipo 'string', 'número' ou 'qualquer').

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

Comentários muito úteis

Datilografado 3.0.1, foi mordido por isso.
Quero um registro que aceite symbol mas o TS não me permite.

Já se passaram 3,5 anos desde que esta edição foi aberta, podemos ter símbolos agora, por favor 🙏

A ironia é que o TS se contradiz.
TS expande keyof any = number | string | symbol .

Mas então quando você faz record[symbol] TS se recusa a dizer
_O tipo de 'símbolo' não pode ser usado como um indexador_.

Todos 93 comentários

Isso faz parte do suporte do símbolo ES6 no qual @JsonFreeman está trabalhando. Seu exemplo de código deve ter suporte na próxima versão.

@wereHamster , com a solicitação pull # 1978, isso deve se tornar legal e obj[theAnswer] terá o tipo any . Isso é suficiente para o que você procura ou precisa de uma digitação mais forte?

Será possível especificar o tipo de propriedades que são indexadas por símbolos? Algo como o seguinte:

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

Com base nos comentários desse PR, não:

_Isso não cobre indexadores de símbolo, o que permite que um objeto atue como um mapa com chaves de símbolo arbitrárias._

Acho que @wereHamster está falando sobre uma digitação mais forte do que @danquirk. Existem 3 níveis de suporte aqui. O nível mais básico é fornecido pelo meu PR, mas é apenas para símbolos que são propriedades do objeto Symbol global, não símbolos definidos pelo usuário. Então,

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

O próximo nível de suporte seria permitir um indexador de símbolo:

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

Isso está em nosso radar e pode ser implementado facilmente.

O nível mais forte é o que você está pedindo, algo como:

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

Isso seria muito bom, mas até agora não temos um design sensato para ele. Em última análise, parece depender de fazer o tipo depender do valor de tempo de execução desses símbolos. Continuaremos a pensar nisso, pois é claramente uma coisa útil a se fazer.

Com meu PR, você deve pelo menos ser capaz de usar um símbolo para extrair um valor _out_ de um objeto. Será any , mas você não receberá mais um erro.

@wereHamster Eu fiz um pequeno artigo # 2012 que você pode estar interessado.

Eu mesclei a solicitação nº 1978, mas deixarei esse bug aberto, pois parece pedir mais do que eu forneci com essa alteração. No entanto, com minha mudança, o erro original irá embora.

@wereHamster você pode postar uma atualização do que mais você gostaria de ver acontecer aqui? Não ficou claro para mim o que implementamos em comparação com o que você postou

Alguma ideia de quando symbol será um tipo válido como indexador? Isso é algo que poderia ser feito como RP da comunidade?

Faríamos um PR para isso. @JsonFreeman pode fornecer detalhes sobre alguns dos problemas que você pode encontrar.

Na verdade, acho que adicionar um indexador de símbolo seria bastante simples. Funcionaria exatamente como número e string, exceto que não seria compatível com nenhum deles em atribuibilidade, inferência de argumento de tipo, etc. O principal desafio é apenas garantir que você se lembre de adicionar lógica em todos os lugares apropriados.

@RyanCavanaugh , seria bom eventualmente ter o último exemplo em https://github.com/Microsoft/TypeScript/issues/1863#issuecomment -73668456 typecheck. Mas, se preferir, você pode dividir esse problema em vários problemas menores que se complementam.

Houve alguma atualização nesta frente? AFAIU a versão mais recente do compilador suporta apenas o primeiro nível descrito em https://github.com/Microsoft/TypeScript/issues/1863#issuecomment -73668456.

Ficaríamos felizes em aceitar PRs para essa mudança.

Pode valer a pena rastrear os dois níveis como dois problemas separados. Os indexadores parecem bastante diretos, mas a utilidade não é clara. O suporte completo com rastreamento constante parece bastante difícil, mas provavelmente mais útil.

O rastreamento constante já é rastreado em https://github.com/Microsoft/TypeScript/issues/5579. esse problema é para adicionar suporte para um indexador de símbolo, semelhante a indexadores de string e numéricos.

Entendi, faz sentido.

@JsonFreeman @mhegazy um problema está disponível em # 12932

Só pensei em jogar meu caso de uso no ringue. Estou escrevendo uma ferramenta que permite que as consultas sejam descritas especificando chaves de texto simples para correspondência com propriedades arbitrárias do objeto e símbolos para especificar operadores de correspondência. Ao usar símbolos para operadores conhecidos, evito a ambigüidade de comparar um operador com um campo cuja chave é a mesma do operador conhecido.

Como os símbolos não podem ser especificados como chaves de índice, ao contrário do que o JavaScript permite explicitamente, sou forçado a lançar para <any> em vários lugares, o que degrada a qualidade do código.

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

O uso de primeiros caracteres "improváveis", como $ , não é um meio-termo adequado, dada a variedade de dados que o mecanismo de consulta pode precisar inspecionar.

Oi pessoal. Existe algum movimento nisso? Eu preciso disso, então ficaria feliz em contribuir com as mudanças necessárias. Porém, não contribuiu para o TS antes.

@mhegazy @RyanCavanaugh Eu sei que vocês são incrivelmente ocupados, mas vocês poderiam pesar quando tiverem uma chance? Os símbolos são uma ferramenta realmente importante para arquitetar bibliotecas e frameworks, e a falta de habilidade para usá-los em interfaces é um ponto crítico.

Estou perguntando se há algo em andamento? Espero sinceramente que esse recurso seja compatível.

Sim, ainda estou procurando por isso hoje, isso é o que vejo no Webstorm:

screenshot 2017-10-08 21 37 17

Isso realmente funciona

var test: symbol = Symbol();

const x = {
    [test]: 1
};

x[test];

console.log(x[test]);

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

mas o tipo de x não está certo, sendo inferido como

{
  [key: string]: number
}

Sim, ainda estou procurando por isso hoje, isso é o que vejo no Webstorm:

Observe que o serviço de idioma do próprio JetBrains, que é habilitado por padrão no WebStorm, intelliJ IDEA e assim por diante.

Isso funciona no TS 2.7

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

alguma atualização disso?

Meu problema:

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

Mas isso também não funciona, porque o símbolo não pode ser um tipo de índice.

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

Comportamento esperado:
symbol deve ser um tipo de índice válido

Comportamento real:
symbol não é um tipo de índice válido

Usar a solução alternativa para moldar as string | number parece muito ruim para mim.

Como util.promisify.custom deve ser usado no TypeScript? Parece que agora há suporte para o uso de símbolos constantes, mas apenas se forem definidos explicitamente. Portanto, este é um TypeScript válido (além de f não ter sido inicializado):
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()
Mas se promisify.custom for usado em vez de custom , a tentativa de referenciar f[promisify.custom] resulta no erro 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()
Eu gostaria de atribuir ao campo promisify.custom uma função, mas parece (dado o comportamento descrito acima) que a única maneira de fazer isso é converter a função para um tipo any .

Não consigo entender por que o símbolo não é permitido como índice de chave, o código abaixo deve funcionar e é aceito pelo Typescript 2.8, mas não é permitido pelo 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 = {};
  }
}

Tentei abaixo, o que é mais "correto" para mim, mas não é aceito por ambas as versões do compilador 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 = {};
  }
}

O status deste tíquete indica que o que você está sugerindo é o comportamento desejado, mas a equipe principal não está neste momento comprometendo recursos para adicionar este aprimoramento de recurso, eles estão abrindo para a comunidade resolver.

@beenotung Embora esta não seja uma solução ideal, supondo que a classe que você postou seja o único lugar onde você precisa de tal comportamento, você pode fazer projeções inseguras dentro da classe, mas mantendo as assinaturas de classe e métodos iguais , para que os consumidores da classe não verá isso:

/**
 * 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 = {};
  }
}

Como as assinaturas são iguais, sempre que usar esta classe, você terá a validação do tipo aplicada corretamente e, quando esse problema for resolvido, você só precisará alterar esta classe (será transparente para os consumidores).

Um exemplo de consumidor é o seguinte (o código não precisará de nenhuma alteração quando esse problema for corrigido):

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

Datilografado 3.0.1, foi mordido por isso.
Quero um registro que aceite symbol mas o TS não me permite.

Já se passaram 3,5 anos desde que esta edição foi aberta, podemos ter símbolos agora, por favor 🙏

A ironia é que o TS se contradiz.
TS expande keyof any = number | string | symbol .

Mas então quando você faz record[symbol] TS se recusa a dizer
_O tipo de 'símbolo' não pode ser usado como um indexador_.

Sim, tenho sofrido com este há algum tempo, infelizmente, minha última pergunta sobre este tópico:

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

@RyanCavanaugh @DanielRosenwasser @mhegazy Alguma atualização? Esta edição está chegando ao seu quarto aniversário.

Se alguém pudesse me indicar a direção certa, eu posso tentar. Se houver testes compatíveis, melhor ainda.

@jhpratt há um PR no # 26797 (observe a advertência sobre símbolos bem conhecidos). Existem notas de reuniões de design recentes sobre isso no nº 28581 (mas nenhuma resolução registrada lá). Há um pouco mais de feedback sobre por que esse PR é mantido aqui . Parece ser considerado uma questão secundária / de baixo impacto, então talvez mais votos positivos no RP possam ajudar a destacar a questão.

Obrigado @yortus. Acabei de perguntar a Ryan se o PR ainda está planejado para o 3.2, que é o que é indicado pelo marco. Esperançosamente, esse é o caso e isso será resolvido!

O PR apontado por @yortus parece uma grande mudança,
A correção para esse bug não deveria ser muito pequena? por exemplo, adicionar uma instrução ou na verificação de condição.
(Ainda não localizei o lugar para mudar.)

solução temporária aqui https://github.com/Microsoft/TypeScript/issues/24587#issuecomment -412287117, meio feio, mas dá conta do recado

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

outro https://github.com/Microsoft/TypeScript/issues/24587#issuecomment -460650063, desde 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' }

Acho que uma das pegadinhas que observei no uso de símbolos é que se você tiver um projeto envolvendo o módulo child_process, sim, você pode compartilhar tipos / enums / interfaces entre os dois processos, mas nunca símbolos.

é muito bom ter isso resolvido, porém, os símbolos são realmente ótimos no rastreamento de objetos sem poluir suas chaves e sendo obrigados a usar mapas / conjuntos, além disso, os benchmarks nos últimos anos mostram que acessar símbolos é tão rápido quanto acessar string / teclas numéricas


Editar: Acontece que esta abordagem só funciona com Record<X,Y> mas não com Interfaces . Acabou usando // @ts-ignore por enquanto, uma vez que ainda está sintaticamente correto e ainda compila bem para JS como deveria.

Uma coisa a se notar, entretanto, é que ao usar // @ts-ignore em linhas envolvendo símbolos, é realmente possível (e ajuda) especificar manualmente o tipo de símbolo. O VSCode ainda pega isso.

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

Não sei, se alguém começou algo para consertar isso, mas se não, eu comecei agora.

No entanto, meu tempo é limitado e não tenho nenhum conhecimento sobre as fontes do texto datilografado. Fiz um fork (https://github.com/Neonit/TypeScript), mas nenhuma solicitação pull, ainda, porque não quero molestar os desenvolvedores com alterações inacabadas (?). Peço a todos que contribuam com o que puderem para o meu fork. Eu irei eventualmente emitir um PR então.

Até agora, encontrei uma maneira de corrigir a restrição de tipo de índice da interface. Eu não sei, se há mais do que isso. Consegui indexar um objeto com um símbolo no TS 3.4 sem nenhuma correção. ( 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)

Dê uma olhada em meu commit para ver o que encontrei: https://github.com/Neonit/TypeScript-SymbolKeys/commit/11cb7c13c2494ff32cdec2d4f82673058c825dc3

Ausência de:

  • Testes: Não tive tempo de ver como os testes em TS são organizados e estruturados.
  • Localização: a mensagem de diagnóstico foi atualizada apenas para a variante em inglês. Talvez as outras línguas ainda recebam a mensagem antiga. Eu não sei. Eu só pude fornecer uma tradução alemã de qualquer maneira.

Espero que isso dê início a tudo, finalmente, após anos de espera.

a correção parece boa. o desenvolvedor do TypeScript pode dar uma olhada nele?

Olá, algum progresso para isso?

Acabei de abrir um tópico do SO sobre isso: https://stackoverflow.com/questions/59118271/using-symbol-as-object-key-type-in-typescript

Por que isso não é possível? symbol outro tipo primitivo como number - então por que há uma diferença?

Olá, algum progresso para isso?

CINCO anos se passaram!

Você não vai acreditar quanto tempo levou para C ++ fechar 😲

lol justo, mas C ++ não está se promovendo como um superconjunto de uma linguagem que tem fechamentos :-p

@ljharb continue batendo naquele cavalo, ele ainda está se contorcendo 😛

Para aqueles que buscam tempos de execução mais recentes, por que não usar Map ? Eu descobri anedoticamente que muitos desenvolvedores não sabem que Map s existem, então estou curioso para saber se há outro cenário que estou perdendo.

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

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


Mapas e objetos têm diferentes casos de uso.

Os símbolos bem conhecidos Symbol.match , por exemplo, não tornará o objeto semelhante a RegExp (e qualquer objeto pode querer uma chave Symbol.iterable para torná-lo iterável sem ter que usar explicitamente o TS integrado Tipos iteráveis).

Quase 5 anos (

Por favor, implemente este recurso, não consigo escrever código normalmente ..

Os participantes podem fornecer exemplos reais em seus casos de uso?

Não entendo o exemplo de protocolo e por que não é possível hoje.

Aqui está um exemplo de 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]())

Aqui está um exemplo de Mappable ou Functor (à parte, falta de construtores de tipo de ordem superior):

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 parece que você está assumindo que todos os objetos têm uma interface ou são uma instância de classe ou são conhecidos antecipadamente; usando seu último exemplo, devo ser capaz de instalar map , digamos, em qualquer objeto javascript (ou, pelo menos, um objeto cujo tipo permite que qualquer símbolo seja adicionado a ele), o que o torna mapeavel.

A instalação de uma propriedade (símbolo ou não) em um objeto após o fato faz parte de uma solicitação de recurso diferente (geralmente chamada de "propriedades expando" ou "tipos de expando").

Sem isso, o tipo que você precisa para uma assinatura de índice de símbolo forneceria muito pouco para um usuário TypeScript, certo? Se bem entendi, o tipo precisa ser algo como unknown ou apenas any para ser útil.

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

No caso de protocolos, geralmente é uma função, mas com certeza, pode ser unknown .

O que eu preciso é o símbolo (e bigint) equivalente a type O = { [k: string]: unknown } , para que eu possa representar um objeto JS real (algo que pode ter qualquer tipo de chave) com o sistema de tipos. Posso restringir isso mais tarde, conforme necessário, mas o tipo base para um objeto JS seria { [k: string | bigint | symbol | number]: unknown } , essencialmente.

Ah, acho que vejo o ponto de @DanielRosenwasser . Atualmente, tenho um código com uma interface como:

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

onde Intrinsic<T> é um tipo de função, e eu quero permitir que os desenvolvedores definam suas próprias propriedades de símbolo em ambientes semelhantes a strings, mas desde que você possa adicionar [Symbol.iterator] , [Symbol.species] ou propriedades de símbolo personalizadas para qualquer interface, a assinatura de índice com símbolos restringiria incorretamente quaisquer objetos que implementassem essas propriedades.

Então, o que você está dizendo é que não pode tornar o tipo de valor de indexação por símbolo mais específico do que any ? Poderíamos de alguma forma usar a distinção unique symbol vs symbol para permitir isso? Como poderíamos tornar a assinatura do índice um padrão para símbolos regulares e permitir que símbolos únicos / bem conhecidos substituam o tipo de índice? Mesmo que não fosse de tipo, seria útil obter / definir propriedades por índices de símbolo arbitrariamente.

A alternativa seria fazer com que os próprios usuários estendessem a interface do ambiente com suas propriedades de símbolo, mas isso não fornece nenhuma segurança de tipo adicional, na medida em que os usuários podem digitar o símbolo como qualquer coisa no objeto.

@DanielRosenwasser aqui um exemplo real do meu código de produção. Um estado reutilizado em muitos lugares como um mapa e pode aceitar a chave do átomo (recurso de domínio). Atualmente, preciso adicionar suporte a símbolos, mas recebo muitos erros:


De qualquer forma, o comportamento atual é incompatível com o padrão ES que está errado.

Um pensamento adicional tarde da noite que tive sobre os tipos de símbolos. Por que isso não é um erro?

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

JS espera que todas as propriedades Symbol.iterator sejam uma função que retorna um iterador, e este objeto quebraria muitos códigos se fosse passado em vários lugares. Se houvesse uma maneira de definir globalmente as propriedades do símbolo para todos os objetos, poderíamos permitir assinaturas de índice de símbolo específicas ao mesmo tempo que permitiríamos substituições globais. Seria um tipo seguro, certo?

Eu também não estou entendendo porque um caso de uso seria necessário aqui. Esta é uma incompatibilidade ES6, que não deveria existir em um ES6 de agrupamento de linguagem.

No passado, postei minhas descobertas sobre como isso poderia ser corrigido aqui neste tópico e se este código não estiver faltando verificações ou recursos importantes, duvido que demore mais para integrá-lo à base de código do que continuar esta discussão.

Simplesmente não fiz uma solicitação de pull, porque não sei sobre a estrutura de teste ou requisitos do Typescript e porque não sei se mudanças em arquivos diferentes seriam necessárias para fazer isso funcionar em todos os casos.

Portanto, antes de continuar a investir tempo para ler e escrever aqui, verifique se adicionar o recurso consumiria menos tempo. Duvido que alguém reclamasse de estar escrito à máquina.

Além de tudo isso, o caso de uso geral é se você deseja mapear valores em símbolos arbitrários. Ou para compatibilidade com código ES6 não digitado.

Aqui está um exemplo de um lugar que eu acho que isso seria útil: https://github.com/choojs/nanobus/pull/40/files. Na prática, eventName s podem ser símbolos ou strings, então eu gostaria de poder dizer

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

na primeira linha.

Mas posso estar entendendo mal como deveria estar fazendo isso.

O caso de uso simples não pode ser feito sem dor:

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

Eu também não estou entendendo porque um caso de uso seria necessário aqui.

@neonit, existem PRs para resolver isso, mas meu entendimento é que existem questões sutis sobre como o recurso interage com o resto do sistema de tipos. Na falta de soluções para isso, o motivo pelo qual peço casos de uso é porque não podemos apenas trabalhar / focar em cada recurso que gostaríamos de uma vez - então os casos de uso precisam justificar o trabalho que está sendo feito, o que inclui longo prazo manutenção de um recurso.

Parece que, na verdade, os casos de uso imaginados pela maioria das pessoas não serão resolvidos tão facilmente quanto eles imaginam (veja a resposta de @brainkim aqui https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574550587), ou que eles são resolvidos igualmente bem por meio de propriedades de símbolo (https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574538121) ou Mapas (https://github.com/microsoft/TypeScript/issues/1863 # issuecomment-572733050).

Acho que @ Tyler-Murphy deu o melhor exemplo aqui, em que você não pode escrever restrições, o que pode ser muito útil para algo como um emissor de evento de tipo seguro que suporta símbolos.

Portanto, antes de continuar a investir tempo para ler e escrever aqui, verifique se adicionar o recurso consumiria menos tempo. Duvido que alguém reclamasse de estar escrito à máquina.

Isso é sempre mais fácil de dizer quando você não precisa manter o projeto! 😄 Eu entendo que isso seja algo útil para você, mas espero que respeite isso.

Esta é uma incompatibilidade ES6

Existem muitas construções que o TypeScript não pode digitar facilmente porque seria inviável. Não estou dizendo que isso seja impossível, mas não acredito que seja uma forma apropriada de enquadrar a questão.

Portanto, parece que a incapacidade de adicionar chaves de símbolo como assinaturas de índice vem do fato de que existem símbolos globais bem conhecidos que exigem suas próprias tipificações, com os quais os tipos de índice de símbolo inevitavelmente entrariam em conflito. Como solução, e se tivéssemos um módulo / interface global que representasse todos os símbolos conhecidos?

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

Ao declarar propriedades adicionais na interface global KnownSymbols , você permite que todos os objetos sejam indexados por aquele símbolo e restringe o valor da propriedade a indefinido / seu tipo de valor. Isso forneceria valor imediatamente, permitindo que a digitação fornecesse digitações para os símbolos conhecidos fornecidos pelo ES6. Adicionar uma propriedade Symbol.iterator a um objeto que não é uma função que retorna um iterador deve ser claramente um erro, mas não está atualmente em texto digitado. E tornaria muito mais fácil adicionar propriedades de símbolo conhecidas a objetos já existentes.

Esse uso de um módulo global também permite que os símbolos sejam usados ​​como chaves arbitrárias e, portanto, em assinaturas de índice. Você apenas daria precedência às propriedades do símbolo global conhecido sobre o tipo de assinatura de índice local.

A implementação desta proposta permitiria que os tipos de assinatura de índice avançassem?

Os casos de uso individuais são irrelevantes. Se for JavaScript cromulento, ele precisa ser expressado em definições de TS.

mas meu entendimento é que existem problemas sutis em como o recurso interage com o resto do sistema de tipos

Mais como "refatora como as assinaturas de índice funcionam inteiramente internamente, então é uma grande mudança assustadora e levanta questões cromuletas sobre como as assinaturas de índice são ou deveriam ser diferentes dos tipos mapeados que não usam a variável de modelo" para ser preciso.

Isso levou principalmente a uma discussão sobre como não reconhecemos tipos fechados versus tipos abertos. Nesse contexto, um tipo "fechado" seria um tipo com um conjunto finito de chaves cujos valores não podem ser estendidos. As chaves de um tipo exato, se preferir. Enquanto isso, um tipo "aberto" neste contexto é um tipo que, quando subtipado, está aberto a ter mais chaves adicionadas (o que, de acordo com nossas regras de subtipagem atuais, sorta todos os tipos são principalmente às vezes, exceto tipos com assinaturas de índice que explicitamente são) . As assinaturas de índice implicam em fazer um tipo aberto, enquanto os tipos mapeados são amplamente relacionados como se operassem sobre tipos fechados. Isso _usualmente_ funciona bem porque a maioria dos códigos, na prática, é escrita com uma estrutura compatível com tipos de objetos fechados. É por isso que flow (que tem sintaxe explícita para tipos de objetos fechados vs abertos) padroniza para tipos de objetos fechados. Isso vem à tona com chaves de índice genéricas; Se eu tiver um T extends string , como T são tipos cada vez mais amplos (de "a" a "a" | "b" a string ), o objeto produzido é cada vez mais especializado, até que trocamos de "a" | "b" | ... (every other possible string) para string si. Uma vez que isso acontece, de repente o tipo é muito aberto, e embora cada propriedade possa potencialmente existir para acessar, torna-se legal, por exemplo, atribuir um objeto vazio a ele. Isso é o que acontece estruturalmente, mas quando relacionamos os genéricos em tipos mapeados, ignoramos isso - uma restrição string em uma chave de tipo mapeado genérico está essencialmente relacionada como se fizesse todas as chaves possíveis existirem. Isso logicamente segue de uma visão simples baseada em variação do tipo de chave, mas só é correto se as chaves vierem de um tipo _closed_ (que, ofc, um tipo com uma assinatura de índice nunca é realmente fechado!). Portanto, se quisermos ser compatíveis com versões anteriores, _não podemos_ tratar {[x: T]: U} da mesma forma que {[_ in T]: U} , a menos que, ofc, queiramos, já que no caso não genérico {[_ in T]: U} torna-se {[x: T]: U} , ajuste como lidamos com a variação das chaves de tipo mapeado para contabilizar apropriadamente o tipo aberto "borda", que é uma mudança interessante em si mesma que poderia ter ramificações no ecossistema.

Basicamente: como ele aproxima os tipos mapeados e as assinaturas de índice, levantou um monte de questões sobre como lidamos com ambos para as quais ainda não temos respostas satisfatórias ou conclusivas.

Os casos de uso individuais são irrelevantes.

Isso é, educadamente, pura loucura. Como podemos saber se estamos adicionando ou não um recurso com o comportamento que as pessoas desejam, sem casos de uso para julgar esse comportamento?

Não estamos tentando ser difíceis aqui, fazendo essas perguntas; estamos literalmente tentando garantir que implementamos as coisas que as pessoas estão pedindo. Seria uma verdadeira vergonha se implementássemos algo que pensávamos ser "indexação com símbolos", apenas para ter as mesmas pessoas neste tópico voltando e dizendo que fizemos tudo errado porque não abordou seus casos de uso particulares.

Você está nos pedindo para voar às cegas. Por favor, não! Diga-nos para onde você gostaria que o avião fosse!

Que pena, eu poderia ter sido mais claro sobre o que quis dizer; parecia-me que as pessoas sentiam que tinham que justificar seus casos de uso de código reais, em vez do desejo de descrevê-lo com mais precisão por meio do TS

Portanto, se bem entendi, trata-se principalmente do seguinte problema:

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

Agora, o compilador de Typescript veria isso como um conflito, porque Foo[sym] tem um tipo ambivalente. Já temos o mesmo problema com cordas.

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

A maneira como isso é tratado com índices de string é que índices de string específicos não são permitidos, se houver uma especificação geral para chaves de string e seu tipo for incompatível.

Eu acho que para símbolos este seria um problema onipresente, porque ECMA2015 define símbolos padrão como Symbol.iterator , que podem ser usados ​​em qualquer objeto e, portanto, devem ter uma digitação padrão. O que eles estranhamente não têm, aparentemente. Pelo menos o playground não me permite executar o exemplo Symbol.iterator do MDN .

Supondo que seja planejado adicionar tipificações de símbolo predefinidas, isso sempre levaria a uma definição geral [s: symbol]: SomeType a ser inválida, porque os índices de símbolo predefinidos já têm tipos incompatíveis, então não pode existir um tipo geral comum ou talvez seja necessário ser um tipo function , porque a maioria (/ all?) das chaves de símbolo predefinidas são do tipo function .

Um problema com a mistura de tipos de índice gerais e específicos é a determinação do tipo quando o objeto é indexado com um valor desconhecido em tempo de compilação. Imagine que meu exemplo acima com os índices de string fosse válido, então o seguinte seria possível:

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

O mesmo problema se aplica às chaves de símbolo. É impossível determinar o tipo exato de value em tempo de compilação. Se o usuário inserir 'str' , seria number , caso contrário, seria string (pelo menos o texto datilografado esperaria que fosse string , embora provavelmente pode se tornar undefined ). É por isso que não temos esse recurso? Alguém poderia contornar isso dando a value um tipo de união contendo todos os tipos possíveis da definição (neste caso number | string ).

@Neonit Bem, esse não é o problema que interrompeu o progresso em uma implementação, mas é exatamente um dos problemas que eu tentava apontar - que dependendo do que você está tentando fazer, os indexadores de símbolo podem não ser a resposta.

Se esse recurso fosse implementado, os símbolos integrados do ECMAScript não necessariamente arruinariam tudo, porque nem todo tipo usa esses símbolos; mas qualquer tipo que faz definir uma propriedade com um símbolo bem conhecido (ou qualquer símbolo que você mesmo define) provavelmente seria limitado a uma assinatura índice menos útil para símbolos.

Isso é realmente o que devemos ter em mente - os casos de uso "Eu quero usar isso como um mapa" e "Eu quero usar símbolos para implementar protocolos" são incompatíveis do ponto de vista do sistema de tipos. Portanto, se você tiver algo parecido em mente, as assinaturas de índice de símbolo podem não ajudá-lo, e você pode ser melhor servido por meio de propriedades ou mapas de um símbolo explícito.

Que tal algo como um tipo UserSymbol que é apenas symbol menos os símbolos embutidos? A própria natureza dos símbolos garante que nunca haverá colisões acidentais .

Edit: pensando mais sobre isso, símbolos conhecidos são apenas sentinelas que foram implementadas usando Symbol . A menos que o objetivo seja serialização de objetos ou introspecção, o código provavelmente deve tratar essas sentinelas de forma diferente de outros símbolos, porque eles têm um significado especial para a linguagem. Removê-los do tipo symbol provavelmente tornará (a maioria) o código usando símbolos 'genéricos' mais seguro.

@RyanCavanaugh aqui está meu plano de vôo.

Tenho um sistema no qual uso símbolos como este para propriedades.

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

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

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

Neste caso, quero que txMap se encaixe na assinatura de tipo de transact . Mas, que eu saiba, não posso expressar isso hoje. No meu caso, transact faz parte de uma biblioteca que não sabe o que esperar. Eu faço algo assim para propriedades.

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

Posso gerar o conjunto de tipos que cabem transact do esquema e usar isso. Para isso, eu faria algo assim e dependeria da fusão de declarações.

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

Mas seria bom se eu pudesse, pelo menos, recorrer a uma assinatura de índice para símbolos, só espero que transact recebam objetos JavaScript simples. Também uso apenas símbolos do registro de símbolo global neste caso. Eu não uso símbolos privados.


Devo acrescentar que isso é um pouco chato.

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

Seria muito bom se o TypeScript pudesse entender que os símbolos criados por meio da função Symbol.for são realmente os mesmos.


Isso também é super irritante.

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

Essa pequena função de utilidade permite-me impor uma convenção sobre minha tabela de símbolos global. entretanto, não posso retornar um unique symbol , mesmo se ele for criado por meio da função Symbol.for . Por causa da maneira como o TypeScript faz as coisas, está me forçando a renunciar a certas soluções. Eles simplesmente não funcionam. E eu acho isso triste.

Encontrei outro caso de uso em que symbol como um valor de indexação seria útil, ao trabalhar com ES Proxies para criar uma função de fábrica que envolve um objeto com um proxy.

Veja este exemplo:

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

Para coincidir com a assinatura de tipo ProxyConstructor o argumento genérico deve estender Object , mas isso causa erros porque o argumento genérico não é codificado. Portanto, podemos estender a assinatura de tipo:

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

Mas agora ele levantará um erro porque o segundo argumento ( prop ) de get em ProxyHandler é do tipo PropertyKey que por acaso é PropertyKey .

Portanto, não tenho certeza de como fazer isso com o TypeScript devido às restrições desse problema.

@aaronpowell Qual é o problema que você está enfrentando? Vejo que está se comportando bem:

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 Vejo um erro no playground:

image

@aaronpowell o erro aparece quando você ativa o sinalizador 'strict' em 'compilerOptions' em tsconfig.json .

Portanto, na versão atual do compilador de texto digitado, você deve desligar o modo estrito ou converter o destino em any ...

Claro, mas um elenco any não é realmente ideal e desabilitar o modo estrito está apenas afrouxando as restrições na segurança de tipo.

Ao ler as mensagens, imagino que a próxima "solução" provavelmente será "desativar o texto digitado".

Não deveríamos ter que procurar soluções paliativas nem ter que explicar por que precisamos delas.

É um recurso padrão do javascript, portanto, precisamos dele no typescript.

@DanielRosenwasser meu caso de uso é semelhante ao de @aaronpowell - uma aparente incompatibilidade na interface ProxyHandler e as regras do TypeScript me impedindo de digitar corretamente as armadilhas do manipulador de proxy.

Um exemplo resumido que demonstra o problema:

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

Pelo que eu posso dizer, é impossível criar qualquer tipo para target que evite o erro, mas permita apenas objetos que podem ser acessados ​​legitimamente por PropertyKey .

Sou um novato no TypeScript, então, por favor, me perdoe se algo óbvio estiver faltando.

Outro caso de uso: estou tentando ter um tipo {[tag: symbol]: SomeSpecificType} para que os chamadores forneçam um mapa de valores marcados de um tipo específico de uma forma que se beneficie da compactação da sintaxe literal do objeto (embora ainda evite o conflito de nomes riscos de usar strings simples como tags).

Outro caso de uso: estou tentando iterar todas as propriedades enumeráveis ​​de um objeto, símbolos e strings. Meu código atual se parece com isto (nomes obscurecidos):

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])
        }
    }
}

Eu prefiro muito poder fazer apenas isto:

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])
        }
    }
}

Eu também tenho problemas com a indexação com símbolos. Meu código é o seguinte:

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

Segui a solução de @aaronpowell e de alguma forma consegui

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

Lançar para any de symbol não é tão bom assim.

Muito apreciado por quaisquer outras soluções.

@ahnpnl Para esse caso de uso, você estaria melhor usando um WeakMap que símbolos, e os motores otimizariam isso melhor - não modifica o mapa de tipo de target . Você ainda pode ter que lançá-lo, mas seu molde viveria no valor de retorno.

Uma solução alternativa é usar uma função genérica para atribuir valor ...

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

Uma solução alternativa é usar uma função genérica para atribuir valor ...

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

Eu não concordo. Essas três linhas são iguais entre si:

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

@DanielRosenwasser
Eu tenho esse caso de uso, no link do playground, também resolvi usando mapas, mas dê uma olhada, é feio.

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

Link Playground

Usar símbolos aqui elimina a possível substituição acidental que acontece ao usar strings. Isso torna todo o sistema mais robusto e fácil de manter.

Se você tiver uma solução alternativa que funcione com TS e tenha a mesma legibilidade no código, sou todo ouvidos.

Alguma explicação oficial para este problema?

Uma solução alternativa é usar uma função genérica para atribuir valor ...

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

Você está procurando por

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

No entanto, não há como ler x[theAnswer] volta sem um elenco AFAIK, veja o comentário dois abaixo

Pelo amor de Deus, faça disso uma prioridade.

Você está procurando por

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

No entanto, não há como ler x[theAnswer] volta sem um elenco AFAIK

Conforme apontado por mellonis e MingweiSamuel, as soluções alternativas usando a função genérica são:

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

Cinco anos e o símbolo como índice ainda não são permitidos

Encontrou uma solução alternativa para este caso, não é genérico, mas funciona em alguns casos:

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 Qual é a diferença entre o seu exemplo e o de @beenotung?

FYI: https://github.com/microsoft/TypeScript/pull/26797

(Acabei de encontrar - não faço parte da equipe TS.)

Esta página foi útil?
0 / 5 - 0 avaliações