Menegakkan jenis anggota yang diindeks dari objek ScriptScript?

289

Saya ingin menyimpan pemetaan string -> string dalam objek naskah, dan menegakkan bahwa semua kunci memetakan ke string. Sebagai contoh:

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

Apakah ada cara bagi saya untuk menegakkan bahwa nilai harus berupa string (atau tipe apa pun ..)?

Armentage
sumber

Jawaban:

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
Ryan Cavanaugh
sumber
13
Yang menarik dari sintaks ini adalah bahwa semua jenis selain 'string' untuk kunci tersebut sebenarnya salah. Masuk akal, mengingat bahwa peta JS secara eksplisit dikunci oleh string, tetapi itu membuat sintaks agak berlebihan. Sesuatu seperti '{}: string' untuk hanya menentukan jenis nilai akan tampak lebih sederhana, kecuali mereka akan menambahkan dengan cara tertentu untuk memungkinkan pemaksaan kunci secara otomatis sebagai bagian dari kode yang dihasilkan.
Armentage
42
numberjuga diizinkan sebagai tipe pengindeksan
Ryan Cavanaugh
3
Perlu dicatat: kompiler hanya menegakkan tipe nilai, bukan tipe kunci. Anda dapat melakukan hal-hal [15] = 'apa pun' dan itu tidak akan mengeluh.
amwinter
3
Tidak, itu memang memperkuat tipe kunci. Anda tidak dapat melakukan hal-hal [myObject] = 'apa pun' bahkan jika myObject memiliki implementasi toString () yang bagus.
AlexG
7
@RyanCavanaugh Mungkinkah (atau akankah) menggunakan tipe type Keys = 'Acceptable' | 'String' | 'Keys'pengindeksan (kunci)?
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'.
};

Di sini, antarmuka AgeMapmemberlakukan kunci sebagai string, dan nilai sebagai angka. Kata kunci namedapat berupa pengidentifikasi apa saja dan harus digunakan untuk menyarankan sintaks antarmuka / tipe Anda.

Anda dapat menggunakan sintaksis yang serupa untuk memastikan bahwa suatu objek memiliki kunci untuk setiap entri dalam tipe gabungan:

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

Anda dapat, tentu saja, menjadikan ini tipe generik juga!

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

10.10.2018 pembaruan: Lihat jawaban @ dracstaxi di bawah ini - sekarang ada tipe bawaan Recordyang melakukan sebagian besar untuk Anda.

Pembaruan 1.2.2020: Saya sepenuhnya menghapus antarmuka pemetaan yang dibuat sebelumnya dari jawaban saya. @ dracstaxi jawaban membuat mereka sama sekali tidak relevan. Jika Anda masih ingin menggunakannya, periksa riwayat edit.

Sandy Gifford
sumber
1
{[key: number]: T; } bukan typesafe karena secara internal kunci suatu objek selalu berupa string - lihat komentar pada pertanyaan oleh @tep untuk detail lebih lanjut. mis. Berjalan x = {}; x[1] = 2;di Chrome kemudian Object.keys(x)mengembalikan ["1"] dan JSON.stringify(x)mengembalikan '{"1": 2}'. NumberKasing sudut dengan typeof (mis. Infinity, NaN, 1e300, 999999999999999999999 dll, dll) dapat dikonversi ke kunci string. Juga berhati-hatilah kasus sudut lain untuk kunci string seperti x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
robocat
@robocat Ini benar, dan saya telah bolak-balik mengedit untuk menghapus antarmuka nomor kunci dari jawaban ini untuk sementara waktu. Pada akhirnya saya telah memutuskan untuk menyimpannya karena TypeScript secara teknis dan khusus memungkinkan angka-sebagai-kunci. Karena itu, saya berharap bahwa siapa pun yang memutuskan untuk menggunakan objek yang diindeks dengan angka melihat komentar Anda.
Sandy Gifford
75

Pembaruan cepat: karena Typefrip 2.1 ada tipe bawaan Record<T, K>yang berfungsi seperti kamus.

Contoh dari dokumen:

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

TypeScript 2.1 Dokumentasi aktif Record<T, K>

Satu-satunya kelemahan saya melihat ke menggunakan lebih ini {[key: T]: K}adalah bahwa Anda dapat mengkodekan info berguna pada jenis kunci yang Anda gunakan di tempat "kunci" misalnya jika objek Anda hanya memiliki kunci utama Anda bisa mengisyaratkan bahwa seperti: {[prime: number]: yourType}.

Berikut adalah regex yang saya tulis untuk membantu konversi ini. Ini hanya akan mengkonversi kasus di mana label adalah "kunci". Untuk mengonversi label lain cukup ubah grup penangkap pertama:

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

Menggantikan: Record<$2, $3>

dracstaxi
sumber
3
Ini seharusnya mendapatkan lebih banyak upvotes - ini pada dasarnya adalah versi asli dari jawaban saya.
Sandy Gifford
Apakah record dikompilasi menjadi a {}?
Douglas Gaskell
@DouglasGaskell tidak memiliki kode kompilasi. Records (tidak seperti, katakanlah, Javascript Maps) hanya menyediakan cara untuk menegakkan konten objek literal. Anda tidak bisa new Record...dan const blah: Record<string, string>;akan dikompilasi ke const blah;.
Sandy Gifford
Anda bahkan tidak dapat membayangkan seberapa banyak jawaban ini membantu saya, terima kasih banyak :)
Federico Grandi
Patut disebutkan bahwa serikat pekerja string juga berfungsi Record: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Sandy Gifford
10

Jawaban @Ryan Cavanaugh benar-benar baik dan masih valid. Masih layak untuk menambahkan bahwa pada Fall'16 ketika kita dapat mengklaim bahwa ES6 didukung oleh sebagian besar platform, hampir selalu lebih baik untuk tetap berpegang pada Peta setiap kali Anda perlu mengaitkan beberapa data dengan beberapa kunci.

Ketika kita menulis let a: { [s: string]: string; }kita perlu mengingat bahwa setelah naskah dikompilasi tidak ada yang namanya mengetik data, itu hanya digunakan untuk kompilasi. Dan {[s: string]: string; } akan dikompilasi menjadi hanya {}.

Yang mengatakan, bahkan jika Anda akan menulis sesuatu seperti:

class TrickyKey  {}

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

Ini tidak akan dikompilasi (bahkan untuk target es6, Anda akan mendapatkannyaerror TS1023: An index signature parameter type must be 'string' or 'number'.

Jadi praktis Anda dibatasi dengan string atau angka sebagai kunci potensial sehingga tidak ada banyak rasa menegakkan memeriksa jenis di sini, terutama mengingat bahwa ketika js mencoba mengakses kunci dengan angka itu mengubahnya menjadi string.

Jadi cukup aman untuk berasumsi bahwa praktik terbaik adalah menggunakan Peta bahkan jika kunci adalah string, jadi saya akan tetap dengan:

let staff: Map<string, string> = new Map();
shabunc
sumber
4
Tidak yakin apakah ini mungkin ketika jawaban ini diposting, tetapi hari ini Anda dapat melakukan ini: let dict: {[key in TrickyKey]: string} = {}- di mana TrickyKeyjenis string literal (misalnya "Foo" | "Bar").
Roman Starkov
Secara pribadi saya suka format naskah asli tetapi Anda benar yang terbaik untuk menggunakan standar. Ini memberi saya cara untuk mendokumentasikan "nama" kunci yang tidak benar-benar dapat digunakan tetapi saya dapat membuat kunci yang disebut sesuatu seperti "patientId" :)
coding
Jawaban ini benar-benar valid, dan membuat poin yang sangat bagus, tetapi saya tidak setuju dengan gagasan bahwa hampir selalu lebih baik menempel pada Mapobjek asli . Peta dilengkapi dengan overhead memori tambahan, dan (yang lebih penting) perlu dibuat secara manual dari setiap data yang disimpan sebagai string JSON. Mereka sering sangat berguna, tetapi tidak murni demi mendapatkan jenis dari mereka.
Sandy Gifford
7

Tentukan antarmuka

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

Menerapkan kunci untuk menjadi kunci spesifik antarmuka Pengaturan

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

Membangun jawaban @ shabunc, ini akan memungkinkan penegakan kunci atau nilai - atau keduanya - menjadi apa pun yang ingin Anda tegakkan.

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

Seharusnya juga berfungsi menggunakan enumbukan typedefinisi.

Roy Art
sumber