Aplicando o tipo dos membros indexados de um objeto Typecript?


289

Gostaria de armazenar um mapeamento de string -> string em um objeto TypeScript e impor que todas as chaves sejam mapeadas para strings. Por exemplo:

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

Existe uma maneira de impor que os valores devem ser cadeias (ou qualquer outro tipo ..)?

Respostas:


519
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

13
O interessante dessa sintaxe é que qualquer tipo diferente de 'string' para a chave está realmente errado. Faz todo o sentido, dado que os mapas JS são explicitamente codificados por strings, mas isso torna a sintaxe um pouco redundante. Algo como '{}: string' para simplesmente especificar os tipos dos valores pareceria mais simples, a menos que eles adicionem de alguma forma para permitir a coerção automática das chaves como parte do código gerado.
Armentage

42
numberTambém é permitido como um tipo de indexação
Ryan Cavanaugh

3
vale a pena notar: o compilador está aplicando apenas o tipo de valor, não o tipo de chave. você pode fazer coisas [15] = 'tanto faz' e não vai reclamar.
amwinter

3
Não, ele aplica o tipo de chave. Você não pode fazer coisas [myObject] = 'tanto faz' mesmo que myObject tenha uma boa implementação de toString ().
AlexG

7
@RyanCavanaugh É possível (ou será) usar a type Keys = 'Acceptable' | 'String' | 'Keys'como um tipo de indexação (chave)?
Calebboyd

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

Aqui, a interface AgeMapimpõe chaves como seqüências de caracteres e valores como números. A palavra-chave namepode ser qualquer identificador e deve ser usada para sugerir a sintaxe da sua interface / tipo.

Você pode usar uma sintaxe semelhante para impor que um objeto tenha uma chave para cada entrada em um tipo de união:

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

Obviamente, você também pode fazer deste um 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,
};

Atualização 10.10.2018: Confira a resposta de @ dracstaxi abaixo - agora há um tipo interno Recordque faz a maior parte disso para você.

Atualização 1.2.2020: removi completamente as interfaces de mapeamento pré-fabricadas da minha resposta. A resposta de @ dracstaxi os torna totalmente irrelevantes. Se você ainda deseja usá-los, verifique o histórico de edições.


1
{[tecla: número]: T; } não é tipicamente seguro, porque internamente as chaves de um objeto são sempre uma string - consulte o comentário na pergunta por @tep para obter mais detalhes. Por exemplo, a execução x = {}; x[1] = 2;no Chrome Object.keys(x)retorna ["1"] e JSON.stringify(x)retorna '{"1": 2}'. Os casos de canto com typeof Number(por exemplo, Infinito, NaN, 1e300, 999999999999999999999 etc) são convertidos em chaves de seqüência de caracteres. Também tem cuidado de outros casos de canto para as chaves string como x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
robocat

@robocat Isso é verdade, e eu fui e voltei editando para remover o número de interfaces com chave desta resposta por um tempo. Por fim, decidi mantê-los, já que o TypeScript permite técnica e especificamente números como chaves. Dito isto, espero que quem decida usar objetos indexados com números veja seu comentário.
Sandy Gifford

75

Uma atualização rápida: desde o Typescript 2.1, existe um tipo incorporado Record<T, K>que age como um dicionário.

Um exemplo dos 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 }

Documentação do TypeScript 2.1 em Record<T, K>

A única desvantagem que vejo ao usar isso {[key: T]: K}é que você pode codificar informações úteis sobre o tipo de chave que está usando no lugar de "chave" {[prime: number]: yourType}.

Aqui está um regex que escrevi para ajudar nessas conversões. Isso converterá apenas casos em que o rótulo é "chave". Para converter outros rótulos, basta alterar o primeiro grupo de captura:

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

Substituir: Record<$2, $3>


3
Isso deve ser mais positivo - é essencialmente a versão mais nova e nativa da minha resposta.
Sandy Gifford

O registro é compilado em um {}?
Douglas Gaskell

Os tipos @DouglasGaskell não têm presença no código compilado. Records (diferentemente, digamos, do Javascript Maps) fornecem apenas uma maneira de impor o conteúdo de um objeto literal. Você não pode new Record...e const blah: Record<string, string>;irá compilar const blah;.
Sandy Gifford

Você não pode sequer imaginar o quanto essa resposta me ajudou, obrigado soo muito :)
Federico Grandi

Vale ressaltar que os sindicatos de cordas também funcionam em Records: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford

10

A resposta de @Ryan Cavanaugh é totalmente correta e continua válida. Ainda assim, vale a pena acrescentar que, no outono de 16, quando podemos afirmar que o ES6 é suportado pela maioria das plataformas, quase sempre é melhor seguir o Map sempre que você precisar associar alguns dados a alguma chave.

Quando escrevemos let a: { [s: string]: string; }, precisamos lembrar que, depois da compilação do texto datilografado, não existem dados do tipo, eles são usados ​​apenas para compilar. E {[s: string]: string; } será compilado para apenas {}.

Dito isto, mesmo se você escrever algo como:

class TrickyKey  {}

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

Isso simplesmente não será compilado (mesmo que target es6você obtenhaerror TS1023: An index signature parameter type must be 'string' or 'number'.

Portanto, praticamente você está limitado a uma string ou um número como chave potencial, portanto não há muito senso de impor a verificação de tipo aqui, especialmente tendo em mente que quando js tenta acessar a chave pelo número, ele a converte em string.

Portanto, é bastante seguro supor que a melhor prática é usar o Map, mesmo que as chaves sejam string, então eu continuaria com:

let staff: Map<string, string> = new Map();

4
Não tenho certeza se isso foi possível quando esta resposta foi publicada, mas hoje você pode fazer o seguinte: let dict: {[key in TrickyKey]: string} = {}- onde TrickyKeyé um tipo literal de string (por exemplo "Foo" | "Bar").
Roman Starkov 27/11

Pessoalmente, gosto do formato datilografado nativo, mas você está certo em usar o padrão. Isso me fornece uma maneira de documentar a chave "nome" que não é realmente utilizável, mas posso fazer com que a chave seja chamada "PatientId" :)
codificando

Essa resposta é absolutamente válida e apresenta argumentos muito bons, mas eu discordo da ideia de que quase sempre é melhor manter os Mapobjetos nativos . Os mapas vêm com sobrecarga adicional de memória e (mais importante) precisam ser instanciados manualmente a partir de qualquer dado armazenado como uma string JSON. Eles geralmente são muito úteis, mas não puramente para obter tipos deles.
Sandy Gifford

7

Definir interface

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

Aplicar chave para ser uma chave específica da interface Configurações

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

3

Com base na resposta de @ shabunc, isso permitiria impor a chave ou o valor - ou ambos - para qualquer coisa que você queira impor.

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

Também deve funcionar usando em enumvez de uma typedefinição.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.