Avatar ·

Как задать тип для всех значений объекта в TypeScript, кроме ключей?

Всем привет! У меня есть объект, который я использую как словарь. Ключи — это строки, а значения — числа. Вот так: ```typescript const myDict = { apple: 5, banana: 3, cherry: 8 }; ``` Проблема в том, что TypeScript не проверяет, что все значения в этом объекте — числа. Если я случайно напишу `apple: "five"`, то ошибки не будет, потому что TypeScript выводит тип как `{ apple: string | number; banana: number; cherry: number }`, а не как `{ [key: string]: number }`. Я пробовал использовать `Record`: ```typescript const myDict: Record = { apple: 5, banana: 3, cherry: 8 }; ``` Это работает, но теперь я не могу получить автодополнение для ключей. Когда я пишу `myDict.apple`, TypeScript показывает, что тип — `number`, но не подсказывает, какие именно ключи существуют. Мне нужно, чтобы: 1. Все значения были одного типа (например, `number`). 2. TypeScript знал точные ключи (строка "apple", "banana", "cherry") и давал автодополнение. Как это сделать? Может быть, есть какой-то специальный тип? Я слышал про `Record` и `index signatures`, но не понимаю, как их совместить. Подскажите, пожалуйста, как правильно типизировать такой объект, чтобы и значения были одного типа, и автодополнение по ключам работало? Заранее спасибо! <<>>

Используйте Record с конкретным union-типом ключей

Вы почти у цели. Проблема в том, что Record<string, number> — это слишком широкая сигнатура. Она говорит TypeScript: "любая строка — это ключ, и значение всегда число". Но автодополнение для конкретных ключей теряется, потому что TypeScript не знает, какие именно строки существуют.

Решение — создать union-тип из точных ключей и использовать его как параметр для Record. Вот как это делается:

type FruitKeys = 'apple' | 'banana' | 'cherry';

const myDict: Record<FruitKeys, number> = {
  apple: 5,
  banana: 3,
  cherry: 8
};

// Теперь myDict.apple — это number, и автодополнение работает!
// myDict.orange — ошибка компиляции

Если вы не хотите дублировать ключи в типе, можно использовать typeof и keyof:

const myDict = {
  apple: 5,
  banana: 3,
  cherry: 8
} as const; // as const — важно для получения literal-типов ключей

// Теперь myDict — это { readonly apple: 5; readonly banana: 3; readonly cherry: 8 }
// Но мы хотим, чтобы значения были number, а не literal-типами.
// Используем вспомогательный тип:

type DictOfNumbers<T> = {
  [K in keyof T]: number
};

const myDict2: DictOfNumbers<typeof myDict> = {
  apple: 5,
  banana: 3,
  cherry: 8
};
// myDict2.apple — number, автодополнение есть!

Почему это работает?

Ключевая идея — mapped type (отображённый тип). Когда вы пишете Record<K, V>, это эквивалентно { [P in K]: V }. Если K — это union строк, то TypeScript создаёт объект с точными ключами. Если K — это string, то получается индексная сигнатура без конкретных ключей.

Union-тип 'apple' | 'banana' | 'cherry' — это не string, а подмножество строк. Поэтому TypeScript сохраняет автодополнение.

Дополнительные советы

  • Если ключей много, используйте as const для объекта-константы и keyof typeof для автоматического получения union-типа:
const FRUITS = {
  apple: 5,
  banana: 3,
  cherry: 8
} as const;

type FruitKeys = keyof typeof FRUITS; // 'apple' | 'banana' | 'cherry'

const myDict: Record<FruitKeys, number> = { ...FRUITS };
  • Если нужно, чтобы объект был изменяемым (mutable), уберите as const или используйте -readonly в mapped type:
type MutableDict<T> = {
  -readonly [K in keyof T]: number
};

Теперь вы можете менять значения, но тип значений остаётся number:

const myDict3: MutableDict<typeof FRUITS> = { apple: 5, banana: 3, cherry: 8 };
myDict3.apple = 10; // OK
// myDict3.apple = "text"; // Ошибка: Type 'string' is not assignable to type 'number'

Этот подход даёт вам и строгую типизацию значений, и полное автодополнение по ключам. Идеально для словарей, конфигураций и любых объектов с фиксированным набором ключей.

<<>>

Альтернативный подход: интерфейс с индексной сигнатурой и конкретными ключами

Можно объединить индексную сигнатуру (для проверки всех значений) с конкретными ключами (для автодополнения). Но есть нюанс: TypeScript не позволяет одновременно иметь индексную сигнатуру и конкретные обязательные ключи, если индексная сигнатура шире. Однако можно сделать ключи опциональными:

interface MyDict {
  [key: string]: number;
  apple?: number; // опционально, но TypeScript запомнит
  banana?: number;
  cherry?: number;
}

const myDict: MyDict = {
  apple: 5,
  banana: 3,
  cherry: 8
};

// myDict.apple — number | undefined (из-за опциональности)
// Но автодополнение работает!

Минус: все значения становятся number | undefined. Чтобы избежать этого, можно использовать строгий mapped type, как в первом ответе. Этот способ подходит, если ключи могут добавляться динамически, но вы хотите, чтобы IDE подсказывала основные.

Ещё один вариант — использовать const assertion и satisfies (TypeScript 4.9+):

const myDict = {
  apple: 5,
  banana: 3,
  cherry: 8
} satisfies Record<string, number>;

// myDict.apple — number, автодополнение есть
// myDict.orange — ошибка: Property 'orange' does not exist

satisfies проверяет, что объект соответствует типу Record<string, number>, но не расширяет его. Поэтому TypeScript сохраняет точные ключи и выводит тип значений как number (а не number | undefined). Это самый лаконичный способ, если у вас TypeScript 4.9 или новее.