Typescript: Permitir la indexación con símbolos

Creado en 30 ene. 2015  ·  93Comentarios  ·  Fuente: microsoft/TypeScript

TypeScript ahora tiene un modo de destino ES6 que incluye definiciones Symbol . Sin embargo, cuando intento indexar un objeto con un símbolo, aparece un error (un argumento de expresión de índice debe ser de tipo 'cadena', 'número' o 'cualquiera').

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

Comentario más útil

Typecript 3.0.1, fue mordido por esto.
Quiero un registro que acepte symbol pero TS no me lo permite.

Han pasado 3.5 años desde que se abrió este número, ¿podemos tener símbolos ahora, por favor?

La ironía es que TS se contradice.
TS expande keyof any = number | string | symbol .

Pero luego, cuando lo haces record[symbol] TS se niega a decir
_El tipo 'símbolo' no se puede utilizar como indexador_.

Todos 93 comentarios

Eso es parte del soporte del símbolo ES6 en el que está trabajando @JsonFreeman . Su muestra de código debería ser compatible con la próxima versión.

@wereHamster , con la solicitud de extracción # 1978, esto debería ser legal, y obj[theAnswer] tendrá el tipo any . ¿Es suficiente para lo que está buscando o necesita una escritura más fuerte?

¿Será posible especificar el tipo de propiedades que están indexadas por símbolos? Algo parecido a lo siguiente:

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

Basado en los comentarios en ese PR, no:

_Esto no incluye los indexadores de símbolos, que permiten que un objeto actúe como un mapa con claves de símbolos arbitrarias.

Creo que @wereHamster está hablando de una escritura más fuerte que @danquirk. Aquí hay 3 niveles de soporte. El nivel más básico lo proporciona mi PR, pero eso es solo para símbolos que son propiedades del objeto Símbolo global, no símbolos definidos por el usuario. Entonces,

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

El siguiente nivel de soporte sería permitir un indexador de símbolos:

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

Esto está en nuestro radar y se puede implementar fácilmente.

El nivel más fuerte es el que estás pidiendo, que es algo así como:

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

Esto sería realmente bueno, pero hasta ahora no hemos creado un diseño sensato para ello. En última instancia, parece depender de que el tipo dependa del valor de tiempo de ejecución de estos símbolos. Continuaremos pensando en ello, ya que claramente es algo útil.

Con mi PR, al menos debería poder usar un símbolo para extraer un valor _out_ de un objeto. Será any , pero ya no recibirá ningún error.

@wereHamster Hice una pequeña reseña # 2012 que puede interesarle.

He combinado la solicitud # 1978, pero dejaré este error abierto, ya que parece pedir más de lo que proporcioné con ese cambio. Sin embargo, con mi cambio, el error original desaparecerá.

@wereHamster, ¿puedes publicar una actualización de qué más te gustaría que sucediera aquí? No me quedó claro de inmediato qué hemos implementado frente a lo que publicaste

¿Alguna idea de cuándo symbol será un tipo válido como indexador? ¿Es esto algo que se podría hacer como relaciones públicas comunitarias?

Tomaríamos un RP por esto. @JsonFreeman puede proporcionar detalles sobre algunos de los problemas con los que podría encontrarse.

De hecho, creo que agregar un indexador de símbolos sería bastante sencillo. Funcionaría como número y cadena, excepto que no sería compatible con ninguno de ellos en asignabilidad, inferencia de argumentos de tipo, etc. El principal desafío es asegurarse de recordar agregar lógica en todos los lugares apropiados.

@RyanCavanaugh , sería bueno tener eventualmente el último ejemplo en https://github.com/Microsoft/TypeScript/issues/1863#issuecomment -73668456 typecheck. Pero si lo prefiere, puede dividir este problema en varios problemas más pequeños que se acumulan entre sí.

¿Hubo alguna actualización en este frente? AFAIU, la última versión del compilador, solo admite el primer nivel descrito en https://github.com/Microsoft/TypeScript/issues/1863#issuecomment -73668456.

Estaremos encantados de aceptar RP para este cambio.

Podría valer la pena rastrear los dos niveles como dos cuestiones separadas. Los indexadores parecen bastante sencillos, pero la utilidad no está clara. El soporte completo con seguimiento constante parece bastante difícil, pero probablemente más útil.

El seguimiento constante ya se realiza en https://github.com/Microsoft/TypeScript/issues/5579. este problema es para agregar soporte para un indexador de símbolos, similar a los indexadores numéricos y de cadena.

Entendido, tiene sentido.

@JsonFreeman @mhegazy un número está disponible en # 12932

Solo pensé en lanzar mi caso de uso al ring. Estoy escribiendo una herramienta que permite describir consultas especificando claves de texto sin formato para comparar propiedades de objetos arbitrarios y símbolos para especificar operadores de coincidencia. Al usar símbolos para operadores conocidos, evito la ambigüedad de hacer coincidir un operador con un campo cuya clave es la misma que la del operador conocido.

Debido a que los símbolos no se pueden especificar como claves de índice, a diferencia de lo que JavaScript permite explícitamente, me veo obligado a convertir a <any> en varios lugares, lo que degrada la calidad del 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
  }
};

El uso de primeros caracteres "poco probables" como un carácter $ no es un compromiso adecuado, dada la variedad de datos que el motor de consultas puede necesitar inspeccionar.

Hola chicos. ¿Hay algún movimiento en esto? Lo necesito para poder contribuir con los cambios necesarios. Sin embargo, no he contribuido a TS antes.

@mhegazy @RyanCavanaugh Sé que están increíblemente ocupados, pero ¿podrían opinar cuando tengan la oportunidad? Los símbolos son una herramienta realmente importante para la arquitectura de bibliotecas y marcos, y la falta de capacidad para usarlos en interfaces es un claro problema.

Estoy preguntando ¿hay algo en progreso? Sinceramente espero que esta característica sea compatible.

Sí, todavía estoy buscando esto hoy, esto es lo que veo en Webstorm:

screenshot 2017-10-08 21 37 17

Eso realmente funciona

var test: symbol = Symbol();

const x = {
    [test]: 1
};

x[test];

console.log(x[test]);

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

pero el tipo de x no es correcto, se infiere como

{
  [key: string]: number
}

Sí, todavía estoy buscando esto hoy, esto es lo que veo en Webstorm:

Tenga en cuenta que el servicio de idioma propio de JetBrains está habilitado de forma predeterminada en WebStorm, intelliJ IDEA, etc.

Esto funciona en TS 2.7

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

¿Algún avance en esto?

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

Pero esto tampoco funciona, porque el símbolo no puede ser un 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;
}

Comportamiento esperado:
symbol debe ser un tipo de índice válido

Comportamiento real:
symbol no es un tipo de índice válido

Usar la solución alternativa con la conversión de as string | number me parece muy malo.

¿Cómo se supone que se debe usar util.promisify.custom en TypeScript? Parece que ahora se admite el uso de símbolos constantes, pero solo si están definidos explícitamente. Entonces, esto es TypeScript válido (aparte de f no se inicializa):
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()
Pero si se usa promisify.custom lugar de custom , el intento de hacer referencia a f[promisify.custom] da como resultado el error 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()
Me gustaría asignar al campo promisify.custom una función, pero parece (dado el comportamiento descrito anteriormente) que la única forma de hacerlo es convertir la función en un tipo any .

No puedo entender por qué el símbolo no está permitido como índice de clave, el siguiente código debería funcionar y es aceptado por Typescript 2.8 pero no está permitido por 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 = {};
  }
}

Intenté a continuación, que es más 'correcto' para mí, pero no es aceptado por ambas versiones del compilador de 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 = {};
  }
}

El estado de este ticket indica que lo que está sugiriendo es el comportamiento deseado, pero el equipo central en este punto no está comprometiendo recursos para agregar esta mejora de características, lo están abriendo a la comunidad para que lo aborde.

@beenotung Aunque esta no es una solución ideal, asumiendo que la clase que publicó es el único lugar donde necesita tal comportamiento, puede realizar conversiones inseguras dentro de la clase, pero manteniendo las firmas de clases y métodos iguales , para que los consumidores de la clase no verá eso:

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

Debido a que las firmas son las mismas, siempre que use esta clase, tendrá la validación de tipo aplicada correctamente y, cuando se resuelva este problema, solo tendrá que cambiar esta clase (será transparente para los consumidores).

Un ejemplo de un consumidor es como a continuación (el código no necesitará ningún cambio cuando se solucione este problema):

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

Typecript 3.0.1, fue mordido por esto.
Quiero un registro que acepte symbol pero TS no me lo permite.

Han pasado 3.5 años desde que se abrió este número, ¿podemos tener símbolos ahora, por favor?

La ironía es que TS se contradice.
TS expande keyof any = number | string | symbol .

Pero luego, cuando lo haces record[symbol] TS se niega a decir
_El tipo 'símbolo' no se puede utilizar como indexador_.

Sí, he estado sufriendo con este por un tiempo, lamentablemente, mi última pregunta sobre este tema:

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

@RyanCavanaugh @DanielRosenwasser @mhegazy ¿ Alguna actualización? Este número se acerca a su cuarto cumpleaños.

Si alguien pudiera indicarme la dirección correcta, puedo intentarlo. Si hay pruebas que coincidan, aún mejor.

@jhpratt hay un PR en # 26797 (tenga en cuenta la advertencia sobre los símbolos bien conocidos). Hay notas de reuniones de diseño recientes al respecto en # 28581 (pero no se registra ninguna resolución allí). Hay un poco más de retroalimentación sobre por qué esas relaciones públicas se mantienen aquí . Parece ser considerado como un tema marginal / de bajo impacto, por lo que tal vez más votos a favor sobre las relaciones públicas puedan ayudar a elevar el perfil del problema.

Gracias @yortus. Le acabo de preguntar a Ryan si el PR todavía está planeado para 3.2, que es lo que indica el hito. ¡Ojalá sea así, y esto se resolverá!

El PR apuntado por @yortus parece un gran cambio,
¿No debería ser menor la corrección de este error? por ejemplo, agregando una declaración o en la verificación de condición.
(Todavía no he localizado el lugar para cambiarme).

solución temporal aquí https://github.com/Microsoft/TypeScript/issues/24587#issuecomment -412287117, un poco feo pero hace el trabajo

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

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

Supongo que una de las trampas que he notado al usar símbolos es si tiene un proyecto que involucra el módulo child_process, sí, puede compartir tipos / enumeraciones / interfaces entre los dos procesos, pero nunca símbolos.

Sin embargo, es genial tener esto resuelto, los símbolos son realmente geniales para rastrear objetos sin contaminar sus claves y tener que usar mapas / conjuntos, además de eso, los puntos de referencia en los últimos años muestran que acceder a los símbolos es tan rápido como acceder a una cadena / teclas numéricas


Editar: Resulta que este enfoque solo funciona con Record<X,Y> pero no con Interfaces . Terminé usando // @ts-ignore por ahora, ya que todavía es sintácticamente correcto y aún se compila bien en JS tal como debería.

Sin embargo, una cosa a tener en cuenta es que cuando se usa // @ts-ignore en líneas que involucran símbolos, es realmente posible (y ayuda) especificar manualmente el tipo de ese símbolo. VSCode todavía lo capta.

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

No sé, si alguien ha comenzado algo para solucionar este problema, pero si no, lo hice ahora.

Sin embargo, mi tiempo es limitado y no tengo ningún conocimiento sobre las fuentes de TypeScript. Hice una bifurcación (https://github.com/Neonit/TypeScript), pero no una solicitud de extracción, todavía, porque no quiero molestar a los desarrolladores con cambios sin terminar (?). Les pediría a todos que contribuyan con lo que puedan a mi tenedor. Eventualmente emitiré un PR entonces.

Hasta ahora he encontrado una manera de corregir la restricción del tipo de índice de la interfaz. No sé, si hay más. Pude indexar un objeto con un símbolo en TS 3.4 sin ninguna corrección. ( 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)

Eche un vistazo a mi compromiso para ver lo que encontré: https://github.com/Neonit/TypeScript-SymbolKeys/commit/11cb7c13c2494ff32cdec2d4f82673058c825dc3

Desaparecido:

  • Pruebas: no he tenido tiempo de analizar cómo se organizan y estructuran las pruebas en TS.
  • Localización: el mensaje de diagnóstico solo se ha actualizado para la versión en inglés. Tal vez los otros idiomas sigan recibiendo el mensaje antiguo. No lo sé. De todos modos, solo pude proporcionar una traducción al alemán.

Espero que esto haga que las cosas comiencen finalmente después de años de espera.

la solución parece buena. ¿Puede el desarrollador de TypeScript echarle un vistazo?

hola, algún progreso para esto?

Acabo de abrir un hilo SO sobre esto: https://stackoverflow.com/questions/59118271/using-symbol-as-object-key-type-in-typescript

¿Por qué no es posible? ¿No es symbol otro tipo primitivo como number ? Entonces, ¿por qué hay una diferencia?

Hola, ¿algún progreso para esto?

¡ Han pasado

No vas a creer cuánto tiempo le tomó a C ++ obtener cierres 😲

jajaja, pero C ++ no se comercializa como un superconjunto de un lenguaje que tiene cierres :-p

@ljharb sigue golpeando a ese caballo, todavía está temblando 😛

Para aquellos que están apuntando a tiempos de ejecución más nuevos, ¿por qué no usar Map ? He descubierto de manera anecdótica que muchos desarrolladores no saben que existen Map s, así que tengo curiosidad por saber si hay otro escenario que me falta.

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

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


Los mapas y los objetos tienen diferentes casos de uso.

Los símbolos conocidos de Symbol.match , por ejemplo, no hará que el objeto sea similar a una expresión regular (y cualquier objeto puede querer una clave Symbol.iterable para hacerlo iterable sin tener que usar explícitamente TS incorporado Tipos iterables).

Casi 5 años (

Por favor, implemente esta función, no puedo escribir código normalmente.

¿Pueden los participantes proporcionar ejemplos reales en sus casos de uso?

No entiendo el ejemplo del protocolo y por qué no es posible hoy.

Aquí tienes un ejemplo 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]())

Aquí hay un ejemplo de Mappable o Functor (aparte de la falta de constructores de tipo de orden 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 está asumiendo que todos los objetos tienen una interfaz o son una instancia de clase o se conocen de antemano; usando su último ejemplo, debería poder instalar map , digamos, en cualquier objeto javascript (o al menos, un objeto cuyo tipo permita que se le agregue cualquier símbolo), lo que lo hace mapeable.

La instalación de una propiedad (símbolo o no) en un objeto después del hecho es parte de una solicitud de función diferente (a menudo llamada "propiedades expandidas" o "tipos expandidos").

Si no lo tiene, el tipo que necesitaría para una firma de índice de símbolo proporcionaría muy poco como usuario de TypeScript, ¿verdad? Si lo entiendo correctamente, el tipo debería ser algo como unknown o simplemente any para que sea algo útil.

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

En el caso de los protocolos, generalmente es una función, pero seguro, podría ser unknown .

Lo que necesito es el símbolo (y bigint) equivalente a type O = { [k: string]: unknown } , para poder representar un objeto JS real (algo que puede tener cualquier tipo de clave) con el sistema de tipos. Puedo reducir eso más tarde según sea necesario, pero el tipo base para un objeto JS sería { [k: string | bigint | symbol | number]: unknown } , esencialmente.

Ah, creo que veo el punto de @DanielRosenwasser . Actualmente tengo código con una interfaz 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>;
}

donde Intrinsic<T> es un tipo de función, y quiero permitir que los desarrolladores definan sus propias propiedades de símbolo en entornos similares a las cadenas, pero en la medida en que pueda agregar [Symbol.iterator] , [Symbol.species] o propiedades de símbolo personalizadas a cualquier interfaz, la firma de índice con símbolos restringiría incorrectamente cualquier objeto que implemente estas propiedades.

Entonces, ¿lo que está diciendo es que no puede hacer que el tipo de valor de indexación por símbolo sea más específico que any ? ¿Podríamos de alguna manera usar la distinción unique symbol vs symbol para permitir esto? Por ejemplo, ¿podríamos hacer que la firma del índice sea un valor predeterminado para los símbolos regulares y permitir que los símbolos únicos / conocidos anulen el tipo de índice? Incluso si no fuera seguro para los tipos, sería útil poder obtener / establecer propiedades por índices de símbolo arbitrariamente.

La alternativa sería que los usuarios extendieran la interfaz del entorno ellos mismos con sus propiedades de símbolo, pero esto no proporciona ninguna seguridad de tipo adicional en la medida en que los usuarios pueden escribir el símbolo como lo que sea en el objeto.

@DanielRosenwasser aquí un ejemplo real de mi código de producción. Un estado reutilizado en muchos lugares como mapa y puede aceptar la clave del átomo (característica dominada). Actualmente necesito agregar soporte de símbolos, pero recibo muchos errores:


De todos modos, el comportamiento actual es incompatible con el estándar ES que está mal.

Otro pensamiento nocturno que tuve con respecto a los tipos de símbolos. ¿Por qué no es esto un error?

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

JS espera que todas las propiedades Symbol.iterator sean una función que devuelva un iterador, y este objeto rompería una gran cantidad de código si se pasara en varios lugares. Si hubiera una forma de definir globalmente las propiedades de los símbolos para todos los objetos, podríamos permitir firmas de índices de símbolos específicos y al mismo tiempo permitir anulaciones globales. Sería seguro para escribir, ¿verdad?

Tampoco entiendo por qué se necesitaría un caso de uso aquí. Esta es una incompatibilidad de ES6, que no debería existir en un idioma que envuelve ES6.

En el pasado publiqué mis hallazgos sobre cómo se podría solucionar esto aquí en este hilo y si este código no carece de controles o características importantes, dudo que sea más tiempo integrarlo en la base de código que continuar con esta discusión.

Simplemente no hice una solicitud de extracción, porque no conozco el marco de prueba o los requisitos de Typecript y porque no sé si serían necesarios cambios en diferentes archivos para que esto funcione en todos los casos.

Entonces, antes de continuar invirtiendo tiempo para leer y escribir aquí, verifique si agregar la función consumiría menos tiempo. Dudo que alguien se queje de que esté en TypeScript.

Aparte de todo eso, el caso de uso general es si desea asignar valores a símbolos arbitrarios. O para compatibilidad con código ES6 no mecanografiado.

Aquí hay un ejemplo de un lugar que creo que sería útil: https://github.com/choojs/nanobus/pull/40/files. En la práctica, eventName s pueden ser símbolos o cadenas, así que me gustaría poder decir

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

en la primera línea.

Pero podría estar malinterpretando algo sobre cómo debería hacer esto.

El caso de uso simple no se puede hacer sin dolor:

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

Tampoco entiendo por qué se necesitaría un caso de uso aquí.

@neonit hay relaciones por lo que los casos de uso deben justificar el trabajo que se está realizando, lo que incluye a largo plazo mantenimiento de una característica.

Parece que, de hecho, los casos de uso imaginados de la mayoría de las personas no se resolverán tan fácilmente como imaginan (consulte la respuesta de @brainkim aquí https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574550587), o que se resuelven igualmente bien a través de propiedades de símbolos (https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574538121) o Maps (https://github.com/microsoft/TypeScript/issues/1863 # issuecomment-572733050).

Creo que @ Tyler-Murphy dio el mejor ejemplo aquí en el sentido de que no puede escribir restricciones, lo que puede ser muy útil para algo como un emisor de eventos de tipo seguro que admita símbolos.

Entonces, antes de continuar invirtiendo tiempo para leer y escribir aquí, verifique si agregar la función consumiría menos tiempo. Dudo que alguien se queje de que esté en TypeScript.

¡Esto siempre es más fácil de decir cuando no tienes que mantener el proyecto! 😄 Entiendo que esto es algo útil para ti, pero espero que lo respetes.

Esta es una incompatibilidad de ES6

Hay muchas construcciones que TypeScript no puede escribir fácilmente porque sería inviable. No digo que esto sea imposible, pero no creo que sea una forma adecuada de enmarcar este problema.

Por lo tanto, parece que la imposibilidad de agregar claves de símbolo como firmas de índice proviene del hecho de que hay Símbolos conocidos a nivel mundial que requieren sus propios tipos, con los que los tipos de índices de símbolos chocarían inevitablemente. Como solución, ¿qué pasaría si tuviéramos un módulo / interfaz global que representara todos los símbolos conocidos?

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

Al declarar propiedades adicionales en la interfaz global KnownSymbols , permite que todos los objetos sean indexados por ese símbolo y restringe el valor de la propiedad a indefinido / su tipo de valor. Esto proporcionaría valor inmediatamente al permitir que el mecanografiado proporcione mecanografía para los símbolos conocidos proporcionados por ES6. Agregar una propiedad Symbol.iterator a un objeto que no es una función que devuelve un iterador debería ser claramente un error, pero no es uno actualmente en mecanografiado. Y facilitaría mucho la adición de propiedades de símbolo conocidas a objetos ya existentes.

Este uso de un módulo global también permitiría que los símbolos se usen como claves arbitrarias y, por lo tanto, en firmas de índices. Simplemente le daría prioridad a las propiedades de símbolo conocidas globales sobre el tipo de firma de índice local.

¿La implementación de esta propuesta permitiría que los tipos de firmas de índice avancen?

Los casos de uso individuales son irrelevantes. Si es JavaScript cromulent, debe ser expresable en definiciones de TS.

pero tengo entendido que existen problemas sutiles con la forma en que la función interactúa con el resto del sistema de tipos

Más bien, "refactoriza cómo funcionan las firmas de índice internamente por completo, por lo que es un gran cambio aterrador y plantea preguntas cromulet sobre cómo las firmas de índice son o deberían diferir de los tipos mapeados que no usan la variable de plantilla" para ser precisos.

Principalmente condujo a una discusión sobre cómo no reconocemos los tipos cerrados frente a los tipos abiertos. En este contexto, un tipo "cerrado" sería un tipo con un conjunto finito de claves cuyos valores no se pueden extender. Las claves de una especie de tipo exacto, por así decirlo. Mientras tanto, un tipo "abierto" en este contexto es un tipo que, cuando se subtipifica, está abierto a que se agreguen más claves (que, según nuestras reglas de subtipo actuales, todos los tipos son en su mayoría a veces, excepto los tipos con firmas de índice que explícitamente lo son) . Las firmas de índice implican hacer un tipo abierto, mientras que los tipos mapeados están relacionados en gran medida como si estuvieran operando sobre tipos cerrados. Normalmente, esto funciona bastante bien porque la mayoría del código, en la práctica, está escrito con una estructura compatible con los tipos de objetos cerrados. Esta es la razón por la que flow (que tiene una sintaxis explícita para tipos de objetos cerrados frente a abiertos) se predetermina a tipos de objetos cerrados. Esto llega a un punto crítico con las claves de índice genéricas; Si tengo un T extends string , ya que T se instancian tipos más amplios y más amplios (desde "a" a "a" | "b" a string ), el objeto producido es cada vez más especializado, hasta que cambiamos de "a" | "b" | ... (every other possible string) a string . Una vez que eso sucede, de repente el tipo es muy abierto, y aunque cada propiedad puede existir potencialmente para acceder, se vuelve legal, por ejemplo, asignarle un objeto vacío. Eso es lo que sucede estructuralmente, pero cuando relacionamos los genéricos en tipos mapeados, ignoramos eso: una restricción string en una clave de tipo mapeado genérico está esencialmente relacionada como si hiciera que existieran todas las claves posibles. Esto se sigue lógicamente de una vista simple basada en la varianza del tipo de clave, pero solo es correcto si las claves provienen de un tipo _closed_ (que, ofc, ¡un tipo con una firma de índice nunca está realmente cerrado!). Entonces, si queremos ser compatibles con versiones anteriores, _no_podemos_ tratar {[x: T]: U} la misma manera que {[_ in T]: U} , a menos que, ofc, lo deseemos, ya que en el caso no genérico {[_ in T]: U} convierte en {[x: T]: U} , ajuste cómo manejamos la variación de las claves de tipo mapeado para tener en cuenta correctamente el "borde" de tipo abierto, que es un cambio interesante en sí mismo que podría tener ramificaciones en el ecosistema.

Básicamente: debido a que acerca mucho más los tipos mapeados y las firmas de índice, generó un montón de preguntas sobre cómo manejamos ambos para los que aún no tenemos respuestas satisfactorias o concluyentes.

Los casos de uso individuales son irrelevantes.

Esto es, cortésmente, pura locura. ¿Cómo diablos sabemos si estamos agregando una característica con el comportamiento que la gente quiere sin casos de uso para juzgar ese comportamiento?

No estamos tratando de ser difíciles aquí haciendo estas preguntas; literalmente estamos tratando de asegurarnos de implementar lo que la gente pide. Sería una verdadera lástima si implementamos algo que pensamos que es "indexar con símbolos", solo para que las mismas personas en este hilo regresen y digan que lo hicimos totalmente mal porque no abordó sus casos de uso particulares.

Nos estás pidiendo que vuelemos a ciegas. ¡Por favor no lo hagas! ¡Díganos adónde le gustaría que fuera el avión!

Mi mal, podría haber sido más claro sobre lo que quise decir; Me pareció que las personas sentían que tenían que justificar sus casos de uso de código reales, en lugar de su deseo de describirlo con mayor precisión a través de TS

Entonces, si lo entiendo correctamente, se trata principalmente del siguiente problema:

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

Ahora, el compilador de TypeScript vería esto como un conflicto, porque Foo[sym] tiene un tipo ambivalente. Ya tenemos el mismo problema con las cadenas.

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

La forma en que esto se maneja con índices de cadena es que no se permiten índices de cadena específicos, si hay una especificación general para claves de cadena y su tipo es incompatible.

Supongo que para los símbolos esto sería un problema omnipresente, porque ECMA2015 define símbolos estándar como Symbol.iterator , que se pueden usar en cualquier objeto y, por lo tanto, deberían tener una escritura predeterminada. Lo que, curiosamente, aparentemente no tienen. Al menos el patio de recreo no me permite ejecutar el ejemplo Symbol.iterator de MDN .

Suponiendo que se planea agregar tipos de símbolos predefinidos, siempre conduciría a que una definición general [s: symbol]: SomeType no sea válida, porque los índices de símbolo predefinidos ya tienen tipos incompatibles, por lo que no puede existir un tipo general común o tal vez sea necesario. ser un tipo function , porque la mayoría (/ ¿todas?) de las teclas de símbolo predefinidas son del tipo function .

Un problema con la combinación de tipos de índices generales y específicos es la determinación del tipo cuando el objeto se indexa con un valor no conocido en el momento de la compilación. Imagine que mi ejemplo anterior con los índices de cadena sería válido, entonces sería posible lo siguiente:

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

El mismo problema se aplicaría a las teclas de símbolos. Es imposible determinar el tipo exacto de value en tiempo de compilación. Si el usuario ingresa 'str' , sería number , de lo contrario sería string (al menos Typecript esperaría que fuera un string , mientras que probablemente puede convertirse en undefined ). ¿Es esta la razón por la que no tenemos esta función? Se podría solucionar esto dando value un tipo de unión que contenga todos los tipos posibles de la definición (en este caso number | string ).

@Neonit Bueno, ese no es el problema que ha detenido el progreso de una implementación, pero ese es exactamente uno de los problemas que quiero señalar: que, dependiendo de lo que intente hacer, es posible que los indexadores de símbolos no sean la respuesta.

Si se implementara esta característica, los símbolos incorporados de ECMAScript no necesariamente arruinarían todo porque no todos los tipos usan esos símbolos; pero cualquier tipo que definir una propiedad con un símbolo bien conocido (o cualquier símbolo que usted mismo define) es probable que se limita a una firma de índice menos útil para los símbolos.

Eso es realmente lo que hay que tener en cuenta: los casos de uso "Quiero usar esto como un mapa" y "Quiero usar símbolos para implementar protocolos" son incompatibles desde la perspectiva del sistema de tipos. Por lo tanto, si tenía algo así en mente, es posible que las firmas de índice de símbolos no lo ayuden, y es posible que esté mejor atendido a través de propiedades o mapas de símbolos explícitos.

¿Qué pasa con algo como un tipo UserSymbol que es solo symbol menos los símbolos incorporados? La propia naturaleza de los símbolos asegura que nunca habrá colisiones accidentales .

Editar: Pensando en esto más, los símbolos conocidos son solo centinelas que se implementan usando Symbol . A menos que el objetivo sea la serialización de objetos o la introspección, el código probablemente debería tratar a estos centinelas de manera diferente a otros símbolos, porque tienen un significado especial para el lenguaje. Eliminarlos del tipo symbol probablemente hará que (la mayoría) del código que usa símbolos 'genéricos' sea más seguro.

@RyanCavanaugh aquí está mi plan de vuelo.

Tengo un sistema en el que uso símbolos como este para las propiedades.

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

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

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

En este caso, quiero que txMap ajuste al tipo de firma de transact . Pero que yo sepa, no puedo expresar esto hoy. En mi caso, transact es parte de una biblioteca que no sabe qué esperar. Hago algo como esto para las propiedades.

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

Puedo generar el conjunto de tipos que se ajustan a transact del esquema y usarlo. Para eso haría algo como esto y confiaría en la fusión de declaraciones.

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

Pero sería bueno si al menos pudiera recurrir a una firma de índice para los símbolos, solo espero que se entreguen objetos JavaScript simples a transact , también utilizo solo símbolos del registro de símbolos global en este caso. No utilizo símbolos privados.


Debo agregar que esto es un poco molesto.

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

Sería increíble si TypeScript pudiera entender que los símbolos creados a través de la función Symbol.for realidad son los mismos.


Esto también es muy molesto.

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

Esa pequeña función de utilidad me permite hacer cumplir una convención sobre mi tabla de símbolos global. sin embargo, no puedo devolver un unique symbol , incluso si se crea mediante la función Symbol.for . Debido a la forma en que TypeScript hace las cosas, me obliga a renunciar a ciertas soluciones. Simplemente no funcionan. Y creo que eso es triste.

Me encontré con otro caso de uso en el que symbol como valor de indexación sería útil, cuando se trabaja con ES Proxies para crear una función de fábrica que envuelve un objeto con un proxy.

Toma este ejemplo:

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 hacer coincidir la firma de tipo ProxyConstructor el argumento genérico debe extenderse Object , pero eso genera errores porque el argumento genérico no está codificado. Entonces podemos extender la firma de tipo:

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

Pero ahora generará un error porque el segundo argumento ( prop ) de get en ProxyHandler es del tipo PropertyKey que resulta ser PropertyKey .

Así que no estoy seguro de cómo hacer esto con TypeScript debido a las restricciones de este problema.

@aaronpowell ¿Cuál es el problema al que se enfrenta? Veo que se está portando bien:

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 Veo un error en el patio de recreo:

image

@aaronpowell, el error aparece cuando habilita el indicador 'estricto' en 'compilerOptions' en tsconfig.json .

Entonces, en la versión actual del compilador de mecanografiado, debe desactivar el modo estricto o convertir el objetivo en any ...

Claro, pero un elenco de any no es realmente ideal y deshabilitar el modo estricto es solo aflojar las restricciones en la seguridad de tipos.

Leyendo mensajes, imagino que la próxima "solución" probablemente será "deshabilitar el mecanografiado".

No deberíamos tener que buscar soluciones provisionales ni tener que explicar por qué las necesitamos.

Es una característica estándar de javascript, por lo que la necesitamos en mecanografiado.

@DanielRosenwasser mi caso de uso es similar al de @aaronpowell : una aparente falta de coincidencia en la interfaz ProxyHandler y las reglas de TypeScript me impiden escribir correctamente las trampas del controlador de proxy.

Un ejemplo resumido que demuestra el problema:

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

Por lo que puedo decir, es imposible crear cualquier tipo por target que evite el error y que solo permita objetos a los que se pueda acceder legítimamente con PropertyKey .

Soy un novato en TypeScript, así que perdóname si me estoy perdiendo algo obvio.

Otro caso de uso: estoy tratando de tener un tipo {[tag: symbol]: SomeSpecificType} para que las personas que llaman proporcionen un mapa de valores etiquetados de un tipo específico de una manera que se beneficie de la compacidad de la sintaxis literal del objeto (sin dejar de evitar el conflicto de nombres riesgos de usar cadenas simples como etiquetas).

Otro caso de uso: estoy tratando de iterar todas las propiedades enumerables de un objeto, símbolos y cadenas de ambos. Mi código actual se parece a esto (nombres oscurecidos):

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

Yo prefiero muy fuertemente para poder simplemente hacer esto:

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

También tengo problemas con la indexación con símbolos. Mi código es el siguiente:

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

Seguí la solución de @aaronpowell y de alguna manera logré solucionarlo

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

Transmitir a any desde symbol no es tan agradable.

Realmente apreciado por cualquier otra solución.

@ahnpnl Para ese caso de uso, sería mejor usar un WeakMap que los símbolos, y los motores lo optimizarían mejor: no modifica el mapa de tipos de target . Es posible que aún tenga que lanzarlo, pero su elenco viviría en el valor de retorno.

Una solución alternativa es utilizar una función genérica para asignar 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

Una solución alternativa es utilizar una función genérica para asignar 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

No estoy de acuerdo. Estas tres líneas son iguales entre sí:

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

@DanielRosenwasser
Tengo este caso de uso, en el enlace del patio de recreo, también lo resolví usando mapas, pero mira, es feo.

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

Enlace de juegos

El uso de símbolos aquí descarta la posible sobrescritura accidental que ocurre cuando se usan cadenas. Hace que todo el sistema sea más robusto y más fácil de mantener.

Si tiene una solución alternativa que funciona con TS y tiene la misma legibilidad en el código, soy todo oídos.

¿Algún funcionario que explique este problema?

Una solución alternativa es utilizar una función genérica para asignar 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

Estas buscando

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

Sin embargo, no hay una manera de leer x[theAnswer] retroceder sin un elenco AFAIK ver el comentario dos a continuación

Por el amor de Dios, haga de esto una prioridad.

Estas buscando

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

Sin embargo, no hay manera de leer x[theAnswer] sin un elenco AFAIK

Como señalaron mellonis y MingweiSamuel, las soluciones que utilizan la función genérica son:

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 años y el símbolo como índice aún no está permitido

Encontré una solución alternativa en este caso, no es genérico pero funciona en algunos 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 ¿

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

(Lo acabo de encontrar, en realidad no soy parte del equipo de TS).

¿Fue útil esta página
0 / 5 - 0 calificaciones