Я пытаюсь перебрать объект в TypeScript, где тип значения зависит от ключа. Я хочу, чтобы TypeScript сузил тип значения на основе ключа внутри цикла. Вот мой первоначальный код:
interface MyInterface {
name: string;
age: number;
hobbies?: string[];
}
const obj: MyInterface = {
name: 'John',
age: 23,
};
for (const key in obj) {
const value = obj[key];
// TypeScript treats key as string, and value as any
// I want it to treat value as string when key is 'name', number when key is 'age', etc.
}
В цикле for...in
TypeScript рассматривает key
как string
, а value
как any
. Однако я хочу, чтобы он обрабатывал value
как string
, когда key
есть 'name'
, number
когда key
есть 'age'
и string[]
когда key
есть 'hobbies'
.
Я попытался решить эту проблему, создав неуклюже типизированную функцию-генератор, которая выдает пары ключ-значение с их типами:
function* iterateObjectTyped<T extends MyInterface, K extends keyof T>(
obj: T
): Generator<[K, T[K]]> {
for (const key in obj) {
const value = obj[key as unknown as K];
yield [key as unknown as K, value];
}
}
for (const [key, value] of iterateObjectTyped(obj)) {
// now TypeScript treats key as "name" | "age" | "hobbies"
// and value as string | number | string[]
// however:
if (key === 'age') {
value;
// it still treats value as just string | number | string[]
// even though I expected the above check to narrow down the predictions
}
}
Но это не работает так, как ожидалось. Несмотря на то, что TypeScript теперь рассматривает key
как "name" | "age" | "hobbies"
, а value
как string | number | string[]
внутри цикла, он не сужает тип value
на основе key
в проверке if
.
Есть ли способ добиться этого в TypeScript? Я использую TypeScript 5.4.3. Любая помощь будет оценена по достоинству.
@jcalz Да, это то, что я искал.
Типы объектов в TypeScript являются открытыми/расширяемыми/"неточными", а не закрытыми/запечатанными/"точными" (где "неточный"/"точный" — это терминология из Flow). Это означает, что объекту разрешено содержать больше свойств, чем знает TypeScript:
const obj2 = { name: "John", age: 23, a: true, b: null };
const obj3: MyInterface = obj2; // okay
for (const key in obj3) {
const value = obj[key];
// ^? string
}
Здесь obj3
имеет свойства a
и b
, хотя он относится к типу MyInterface
. Таким образом, все, что TypeScript действительно может сказать о key
, это то, что он относится к типу string
и, следовательно, не является известным ключом MyInterface
, и, следовательно, value
относится к типу any
.
См. TypeScript: Object.keys возвращает строку[].
Если хотите, вы можете утверждать, что объект имеет только те ключи, о которых знает TypeScript, это нормально. Просто имейте в виду, что если вы ошиблись и что-то сломалось, вы несете за это ответственность.
В любом случае, если вы хотите дать своему генератору iterateObjectTyped
сигнатуру вызова, чтобы каждый элемент возвращаемого вывода представлял собой запись пар [K, T[K]]
для некоторых K
в keyof T
, вы можете написать это следующим образом:
function* iterateObjectTyped<T extends object>(
obj: T
): Generator<{ [K in keyof T]-?: [K, T[K]] }[keyof T]> {
for (const key in obj) {
const value = obj[key];
yield [key, value];
}
}
Тип { [K in keyof T]-?: [K, T[K]] }[keyof T]
— это так называемый тип распространяемого объекта, как он был придуман в microsoft/TypeScript#47109 . Он перебирает каждый ключ K
из T
, создает [K, T[K]]
, а затем приводит к их объединению.
Итак, если мы позвоним iterateObjectTyped(obj)
, вы увидите:
// function iterateObjectTyped<MyInterface>(obj: MyInterface): Generator<
// ["name", string] | ["age", number] | ["hobbies", string[] | undefined]
// >
Это объединение пар, и поскольку первый элемент каждой пары представляет собой литеральный тип , он действует как дискриминантное свойство дискриминируемого объединения:
for (const [key, value] of iterateObjectTyped(obj)) {
if (key === 'age') {
value; // const value: number
}
}
Это работает так, как и ожидалось, поскольку поддерживает деструктуризацию размеченных объединений на отдельные переменные. Таким образом, проверка key === 'age'
сузит value
до number
.
Вы не можете знать, что ключи только те, что в
keyof T
, смотрите этот вопрос . Если вы хотите дать своему «неудобно типизированному» генератору что-то, что ваш код будет понимать, вы можете сделать это таким образом. Это полностью решает вопрос? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?