Avatar ·

Как создать пару "ключ-значение" (key-value pair) в TypeScript?

Всем привет! Я новичок в TypeScript, перехожу с JavaScript. В JS я часто использовал простые объекты для хранения пар "ключ-значение", например: ```typescript const user = { name: "Анна", age: 30, city: "Москва" }; ``` Но теперь я хочу создать более строгую структуру: мне нужно хранить динамические данные, где ключи и значения могут быть разными типами. Например, я пишу функцию, которая принимает массив пар "ключ-значение" и возвращает объект. В JavaScript я бы сделал так: ```javascript function buildObject(pairs) { const result = {}; for (const [key, value] of pairs) { result[key] = value; } return result; } ``` Но TypeScript ругается на `result[key] = value`, если я не указываю типы. Я попробовал написать интерфейс: ```typescript interface KeyValuePair { key: string; value: any; } ``` Но это не совсем то, что мне нужно — я хочу, чтобы ключ был именно ключом объекта, а не полем. Подскажите, есть ли в TypeScript встроенный тип для пары "ключ-значение"? Как правильно типизировать такие структуры? Заранее спасибо! <<>>

TypeScript и пары "ключ-значение": основные подходы

В TypeScript нет специального встроенного типа KeyValuePair как отдельной сущности, но есть несколько мощных способов работы с такими структурами. Всё зависит от того, что именно вы хотите сделать: сохранить динамический объект, массив кортежей или использовать строгую типизацию для конкретных ключей.

1. Массив кортежей (Tuple) — самый простой способ

Если вам нужна именно пара "ключ-значение" как элемент массива, используйте кортеж (tuple) TypeScript:

// Явно задаём тип: [ключ: string, значение: number]
const pair: [string, number] = ["age", 30];

// Массив таких пар
const pairs: [string, number][] = [
  ["age", 30],
  ["height", 175]
];

2. Динамический объект с Record<K, V>

Для случая, когда вы хотите создать объект с динамическими ключами, используйте встроенный тип Record<Keys, Type>. Это идеально подходит для вашей функции buildObject:

function buildObject<T extends string, U>(pairs: [T, U][]): Record<T, U> {
  const result = {} as Record<T, U>;
  for (const [key, value] of pairs) {
    result[key] = value;
  }
  return result;
}

// Использование
const obj = buildObject([
  ["name", "Анна"],
  ["age", 30],
  ["city", "Москва"]
]);
// Тип obj: Record<"name" | "age" | "city", string | number>

Обратите внимание на as Record<T, U> — это приведение типа, необходимое, потому что пустой объект TypeScript не может вывести как Record. Дженерики T extends string и U позволяют сохранить точные типы ключей и значений.

3. Интерфейс для конкретных пар

Если ключи известны заранее, лучше создать интерфейс с конкретными полями:

interface User {
  name: string;
  age: number;
  city: string;
}

const user: User = {
  name: "Анна",
  age: 30,
  city: "Москва"
};

Это даёт строгую проверку типов и автодополнение в IDE.

4. Тип Map — для сложных ключей

Если ключи не являются строками или числами, или вам нужны методы вроде .get() и .set(), используйте встроенный объект Map:

const userMap = new Map<string, any>();
userMap.set("name", "Анна");
userMap.set("age", 30);

// Или с типизированными значениями
const typedMap = new Map<string, string | number>();
typedMap.set("name", "Анна");
typedMap.set("age", 30);

5. Свой тип KeyValuePair (кастомный)

Вы можете создать свой обобщённый тип для пары:

type KeyValuePair<K extends string | number | symbol, V> = {
  key: K;
  value: V;
};

// Использование
const pair: KeyValuePair<"age", number> = {
  key: "age",
  value: 30
};

Но этот подход менее удобен, чем кортежи, так как требует дополнительной вложенности.

Рекомендация

  • Для динамических объектов — используйте Record<K, V>.
  • Для массива пар — кортежи [K, V][].
  • Для строго известных ключей — интерфейс с конкретными полями.
  • Для сложных ключей — Map<K, V>.

Выберите подход, который лучше всего подходит под вашу задачу. TypeScript даёт гибкость, но лучше всего использовать наиболее строгий тип, который отражает вашу предметную область.

<<>>

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

Если вы хотите сохранить максимальную гибкость, но при этом избежать any, используйте индексную сигнатуру с ограничениями:

interface FlexibleObject {
  [key: string]: string | number | boolean;
}

const data: FlexibleObject = {
  name: "Анна",
  age: 30,
  isActive: true
};

Этот подход позволяет добавлять любые строковые ключи, но значения должны быть одного из перечисленных типов. Недостаток — все ключи считаются строками, даже если вы используете числа.

Для более точного контроля можно использовать утилитарный тип Partial<T> в комбинации с конкретным интерфейсом:

interface UserFields {
  name: string;
  age: number;
  city?: string;
}

// Частичная реализация
const partialUser: Partial<UserFields> = {
  name: "Анна"
};

Этот способ хорош, когда вы знаете возможные ключи, но не все они обязательны. TypeScript сам подскажет, какие поля доступны, и не даст добавить лишние.

Ещё один полезный приём — использовать typeof для вывода типа из существующего объекта:

const example = { name: "Анна", age: 30 };
type ExampleType = typeof example; // { name: string; age: number; }

Это может пригодиться, когда вы создаёте объект в одном месте, а потом хотите типизировать похожие структуры в другом.