Pesquisa de site

Aplicando o tipo dos membros indexados de um objeto Typescript?


Eu gostaria de armazenar um mapeamento de string -> string em um objeto TypeScript, e garantir que todos os valores se mapeiem para strings. Por exemplo:

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

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

Resposta #1

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

Resposta #2

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 AgeMap exige que as chaves sejam strings e os valores sejam números. A palavra-chave name pode ser qualquer identificador e deve ser usada para sugerir a sintaxe da sua interface/tipo.

Você pode usar uma sintaxe semelhante para exigir que um objeto tenha uma chave para cada entrada em um tipo 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",
};

Você pode, é claro, tornar isso um tipo genérico também!

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

Finalmente, você pode usar o identificador de indexação como um tipo em seu tipo indexado:

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

const weekdayMeta: { [DAY in DayOfTheWeek]: { day: DAY; score: number } } = {
  sunday: { day: "sunday", score: 3 },
  monday: { day: "monday", score: 4 },
  tuesday: { day: "tuesday", score: 2 },
  wednesday: { day: "wednesday", score: 0 },
  thursday: { day: "thursday", score: 5 },
  friday: { day: "friday", score: 6 },
  saturday: { day: "sunday", score: 1 }, // ERROR: Type '"sunday"' is not assignable to type '"saturday"'.
};

Atualização 10.10.2018: Confira a resposta de @dracstaxi abaixo - agora existe um tipo embutido Record que faz parte disso para você.

Atualização em 1.2.2020: Eu removi completamente as interfaces de mapeamento pré-fabricadas da minha resposta. A resposta do @dracstaxi as torna totalmente irrelevantes. Se você ainda quiser usá-las, consulte o histórico de edições.

Resposta #3

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

Neste caso, você poderia declarar coisas assim:

var stuff: Record<string, any> = {};

Você também poderia limitar/especificar chaves potenciais unindo tipos literais:

var stuff: Record<'a'|'b'|'c', string|boolean> = {};

Aqui está um exemplo mais genérico usando o tipo record 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 sobre Record<T, K>

A única desvantagem que vejo em usar isso em vez de {[key: T]: K} é que você pode codificar informações úteis sobre que tipo de chave está usando no lugar de "key", por exemplo, se seu objeto tivesse apenas chaves primárias, você poderia indicar isso assim: {[prime: number]: yourType}.

Aqui está um regex que escrevi para ajudar com essas conversões. Isso só irá converter casos onde o rótulo é "key". Para converter outros rótulos, basta mudar o primeiro grupo de captura:

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

Substituir: Record<$2, $3>

Resposta #4

Na verdade, existe um utilitário integrado chamado Record:

    const record: Record<string, string> = {};
    record['a'] = 'b';
    record[1] = 'c'; // leads to typescript error
    record['d'] = 1; // leads to typescript error

Resposta #5

Você pode passar um nome para a chave desconhecida e então escrever seus tipos:

type StuffBody = {
  [key: string]: string;
};

Agora você pode usá-lo na sua verificação de tipo:

let stuff: StuffBody = {};

Mas para FlowType não há necessidade de ter nome:

type StuffBody = {
  [string]: string,
};

Resposta #6

Definir interface

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

Forçar a chave a ser uma chave específica da interface Settings

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

Resposta #7

A resposta de @Ryan Cavanaugh está totalmente ok e ainda é válida. Ainda assim, vale adicionar que a partir do outono de 2016, quando podemos afirmar que o ES6 é suportado pela maioria das plataformas, quase sempre é melhor usar Map sempre que você precisar associar alguns dados a alguma chave.

Quando escrevemos let a: { [s: string]: string; } precisamos lembrar que, depois que o TypeScript é compilado, não existe algo como dados de tipo, ele é usado apenas para compilação. E { [s: string]: string; } será compilado apenas para {}.

Dito isso, mesmo que você vá escrever algo como:

class TrickyKey  {}

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

Isso simplesmente não vai compilar (mesmo para target es6, você receberá erro TS1023: Um tipo de parâmetro de assinatura de índice deve ser 'string' ou 'number'.

Então, na prática, você está limitado a string ou número como chave potencial, então não faz muito sentido impor verificação de tipo aqui, especialmente tendo em mente que, quando o JS tenta acessar uma chave por número, ele a converte para string.

Então é bastante seguro assumir que a melhor prática é usar Map mesmo que as chaves sejam strings, então eu ficaria com:

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

Artigos relacionados