¿Hacer cumplir el tipo de los miembros indexados de un objeto mecanografiado?

289

Me gustaría almacenar una asignación de cadena -> cadena en un objeto de tipografía y hacer que todas las teclas se asignen a cadenas. Por ejemplo:

var stuff = {};
stuff["a"] = "foo";   // okay
stuff["b"] = "bar";   // okay
stuff["c"] = false;   // ERROR!  bool != string

¿Hay alguna manera de hacer cumplir que los valores deben ser cadenas (o cualquier tipo ...)?

Armentage
fuente

Respuestas:

520
var stuff: { [key: string]: string; } = {};
stuff['a'] = ''; // ok
stuff['a'] = 4;  // error

// ... or, if you're using this a lot and don't want to type so much ...
interface StringMap { [key: string]: string; }
var stuff2: StringMap = { };
// same as above
Ryan Cavanaugh
fuente
13
Lo interesante de esta sintaxis es que cualquier tipo que no sea 'string' para la clave es realmente incorrecto. Tiene mucho sentido, dado que los mapas JS están explícitamente codificados por cadenas, pero hace que la sintaxis sea algo redundante. Algo así como '{}: cadena' para especificar simplemente los tipos de valores parecería más simple, a menos que vayan a agregar de alguna manera para permitir la coerción automática de las claves como parte del código generado.
Armentage
42
numbertambién está permitido como un tipo de indexación
Ryan Cavanaugh
3
Vale la pena señalar: el compilador solo aplica el tipo de valor, no el tipo de clave. puedes hacer cosas [15] = 'lo que sea' y no se quejará.
amwinter
3
No, sí impone el tipo de clave. No puede hacer cosas [myObject] = 'lo que sea' incluso si myObject tiene una buena implementación toString ().
AlexG
77
@RyanCavanaugh ¿Es posible (o será) usar un type Keys = 'Acceptable' | 'String' | 'Keys'tipo de indexación (clave)?
calebboyd el
130
interface AgeMap {
    [name: string]: number
}

const friendsAges: AgeMap = {
    "Sandy": 34,
    "Joe": 28,
    "Sarah": 30,
    "Michelle": "fifty", // ERROR! Type 'string' is not assignable to type 'number'.
};

Aquí, la interfaz AgeMapimpone claves como cadenas y valores como números. La palabra clave namepuede ser cualquier identificador y debe usarse para sugerir la sintaxis de su interfaz / tipo.

Puede usar una sintaxis similar para exigir que un objeto tenga una clave para cada entrada en un tipo de unión:

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

type ChoresMap = { [day in DayOfTheWeek]: string };

const chores: ChoresMap = { // ERROR! Property 'saturday' is missing in type '...'
    "sunday": "do the dishes",
    "monday": "walk the dog",
    "tuesday": "water the plants",
    "wednesday": "take out the trash",
    "thursday": "clean your room",
    "friday": "mow the lawn",
};

¡Por supuesto, también puede hacer que este sea un tipo genérico!

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

type DayOfTheWeekMap<T> = { [day in DayOfTheWeek]: T };

const chores: DayOfTheWeekMap<string> = {
    "sunday": "do the dishes",
    "monday": "walk the dog",
    "tuesday": "water the plants",
    "wednesday": "take out the trash",
    "thursday": "clean your room",
    "friday": "mow the lawn",
    "saturday": "relax",
};

const workDays: DayOfTheWeekMap<boolean> = {
    "sunday": false,
    "monday": true,
    "tuesday": true,
    "wednesday": true,
    "thursday": true,
    "friday": true,
    "saturday": false,
};

Actualización del 10.10.2018: consulte la respuesta de @ dracstaxi a continuación: ahora hay un tipo integrado Recordque hace la mayor parte de esto por usted.

Actualización 1.2.2020: he eliminado por completo las interfaces de mapeo prefabricadas de mi respuesta. La respuesta de @ dracstaxi los hace totalmente irrelevantes. Si aún desea utilizarlos, consulte el historial de edición.

Sandy Gifford
fuente
1
{[clave: número]: T; } no es de tipo seguro porque internamente las claves de un objeto son siempre una cadena; vea el comentario sobre la pregunta de @tep para obtener más detalles. por ejemplo, Ejecutar x = {}; x[1] = 2;en Chrome luego Object.keys(x)devuelve ["1"] y JSON.stringify(x)devuelve '{"1": 2}'. Los casos de esquina con typeof Number(por ejemplo, Infinity, NaN, 1e300, 999999999999999999999, etc.) se convierten en teclas de cadena. También tenga cuidado de otros casos de esquina para claves de cadena como x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
robocat
@robocat Esto es cierto, y he ido una y otra vez en la edición para eliminar las interfaces con números de esta respuesta por un tiempo. Finalmente, he decidido mantenerlos, ya que TypeScript permite técnica y específicamente los números como teclas. Dicho esto, espero que cualquiera que decida usar objetos indexados con números vea su comentario.
Sandy Gifford
75

Una actualización rápida: desde Typecript 2.1 hay un tipo incorporado Record<T, K>que actúa como un diccionario.

Un ejemplo de documentos:

// For every properties K of type T, transform it to U
function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>

const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length);  // { foo: number, bar: number, baz: number }

Documentación de TypeScript 2.1 en Record<T, K>

La única desventaja que veo a usar este sobre {[key: T]: K}es que se puede codificar información útil sobre qué tipo de clave que está utilizando en lugar de "clave" por ejemplo, si su objeto sólo tenía las llaves principales que podría hacer alusión a que de esta manera: {[prime: number]: yourType}.

Aquí hay una expresión regular que escribí para ayudar con estas conversiones. Esto solo convertirá casos donde la etiqueta es "clave". Para convertir otras etiquetas, simplemente cambie el primer grupo de captura:

Encontrar: \{\s*\[(key)\s*(+\s*:\s*(\w+)\s*\]\s*:\s*([^\}]+?)\s*;?\s*\}

Reemplazar: Record<$2, $3>

dracstaxi
fuente
3
Esto debería obtener más votos a favor: es esencialmente la versión más nueva y nativa de mi respuesta.
Sandy Gifford
¿El registro se compila en un {}?
Douglas Gaskell el
Los tipos @DouglasGaskell no tienen presencia en el código compilado. Records (a diferencia de, digamos, Javascript Maps) solo proporciona una forma de imponer el contenido de un objeto literal. No puedes new Record...y vas const blah: Record<string, string>;a compilar const blah;.
Sandy Gifford
Ni siquiera puedes imaginar cuánto me ayudó esta respuesta, muchas gracias :)
Federico Grandi
Vale la pena mencionar que los sindicatos de cuerdas también funcionan en Records: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford
10

La respuesta de @Ryan Cavanaugh está totalmente bien y sigue siendo válida. Sin embargo, vale la pena agregar que a partir de Fall'16, cuando podemos afirmar que ES6 es compatible con la mayoría de las plataformas, casi siempre es mejor atenerse a Map siempre que necesite asociar algunos datos con alguna clave.

Cuando escribimos, let a: { [s: string]: string; }debemos recordar que después de compilar el mecanografiado no existen datos de tipo, solo se usan para compilar. Y {[s: cadena]: cadena; } se compilará a solo {}.

Dicho esto, incluso si escribes algo como:

class TrickyKey  {}

let dict: {[key:TrickyKey]: string} = {}

Esto simplemente no se compilará (incluso para target es6, obtendráerror TS1023: An index signature parameter type must be 'string' or 'number'.

Entonces, prácticamente está limitado con una cadena o un número como clave potencial, por lo que no hay una gran sensación de forzar la verificación de tipo aquí, especialmente teniendo en cuenta que cuando js intenta acceder a la clave por número, lo convierte en cadena.

Por lo tanto, es bastante seguro asumir que la mejor práctica es usar Map incluso si las claves son cadenas, por lo que me quedaría con:

let staff: Map<string, string> = new Map();
shabunc
fuente
44
No estoy seguro de si esto fue posible cuando se publicó esta respuesta, pero hoy puede hacer esto: let dict: {[key in TrickyKey]: string} = {}- donde TrickyKeyes un tipo literal de cadena (por ejemplo "Foo" | "Bar").
Roman Starkov
Personalmente, me gusta el formato mecanografiado nativo, pero tienes razón, es mejor usar el estándar. Me da una manera de documentar el "nombre" clave que no es realmente utilizable, pero puedo hacer que la clave se llame algo así como "patientId" :)
codificación del
Esta respuesta es absolutamente válida y tiene muy buenos puntos, pero no estoy de acuerdo con la idea de que casi siempre es mejor quedarse con los Mapobjetos nativos . Los mapas vienen con una sobrecarga de memoria adicional y (lo que es más importante) deben ser instanciados manualmente a partir de cualquier información almacenada como una cadena JSON. A menudo son muy útiles, pero no solo para obtener tipos de ellos.
Sandy Gifford el
7

Definir interfaz

interface Settings {
  lang: 'en' | 'da';
  welcome: boolean;
}

Hacer cumplir la clave para que sea una clave específica de la interfaz de configuración

private setSettings(key: keyof Settings, value: any) {
   // Update settings key
}
Lasithds
fuente
3

Sobre la base de la respuesta de @ shabunc, esto permitiría hacer cumplir la clave o el valor, o ambos, para ser cualquier cosa que desee aplicar.

type IdentifierKeys = 'my.valid.key.1' | 'my.valid.key.2';
type IdentifierValues = 'my.valid.value.1' | 'my.valid.value.2';

let stuff = new Map<IdentifierKeys, IdentifierValues>();

También debería funcionar usando en enumlugar de una typedefinición.

Roy Art
fuente