#typescript
#typescript
Вопрос:
Предположим, у меня есть запись интерфейса, которая выглядит как
interface Record {
id: string;
createdBy: string;
dateCreated: string;
}
и результат интерфейса, который выглядит как:
interface Result<R extends Record> {
records: R[];
referredRercords: { [ref:string]: Record; }
}
теперь каждая запись типа R содержит некоторый ключ K со строковым значением, которое относится к одной из ссылок в referredRecords .
Я хочу, чтобы какая-то функция объединила их в единый объект, например:
mergeRecords<R extends Record, K extends keyof R>(result: Result<R>, refs: [K,string][]) {
return result.records.map((record) => {
let refsObj = refs.reduce((acc,[refProp, toProp]) => {
let key = record[refProp] as string; // ISSUE OCCURS HERE
let val = result.referredRecords[key]; // OR HERE IF CONVERSION EXCLUDED
return Object.assign(acc, {[toProp]: val});
},{});
return Object.assign(record, refsObj);
});
}
typescript жалуется, что тип R [K] недостаточно перекрывается со строкой для преобразования, и если я исключу преобразование, он жалуется, что тип R [K] нельзя использовать для индексации типа { [ref:string]: Record; }
Это работает, но мне это не нравится, поскольку кажется хакерским:
let key = record[refProp] as unknown as string;
Есть ли способ, которым я могу заверить typescript, что тип R [K] действительно является строкой, не выполняя заранее неизвестное преобразование? Или это просто способ справиться с этим?
Редактировать:
это также работает, и мне это нравится несколько лучше, поэтому я пойду с этим, если у кого-то нет чего-то лучшего:
let key = record[refProp];
let val = (typeof key === 'string') ? result.referredRecords[key] : undefined;
Ответ №1:
Обратите внимание , что в стандартной библиотеке уже есть тип с именем Record<K, V>
, который обозначает тип объекта , ключами которого являются K
и значениями которого являются V
. Я собираюсь переименовать ваш тип в MyRecord
, чтобы различать их. На самом деле встроенный Record
тип достаточно полезен, что я, вероятно, в конечном итоге (по совпадению) использую его в ответе на этот вопрос.
Итак, вот новый код:
interface MyRecord {
id: string;
createdBy: string;
dateCreated: string;
}
interface Result<R extends MyRecord> {
records: R[];
referredRecords: { [ref: string]: MyRecord; }
}
function mergeRecords<R extends MyRecord, K extends keyof R>(
result: Result<R>, refs: [K, string][]
) {
return result.records.map((record) => {
let refsObj = refs.reduce((acc, [refProp, toProp]) => {
let key: string = record[refProp]; // error, not a string
let val = result.referredRecords[key];
return Object.assign(acc, { [toProp]: val });
}, {});
return Object.assign(record, refsObj);
});
}
Ваша главная проблема в том, что вы предположили, что R[K]
это будет a string
, но вы не сказали об этом компилятору. Таким образом, текущее определение mergeRecords()
будет с радостью принять это:
interface Oops extends MyRecord {
foo: number;
bar: string;
}
declare const result: Result<Oops>;
mergeRecords(result, [["foo", "bar"]]); // okay, but it shouldn't accept "foo"!
mergeRecords(result, [["bar", "baz"]]); // okay
Упс.
В идеале вы должны ограничить R
(и, возможно K
, ) типы, чтобы компилятор знал, что происходит. Вот один из способов сделать это:
// return the keys from T whose property types match the type V
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T];
function mergeRecords<
R extends MyRecord amp; Record<K, string>,
K extends KeysMatching<R, string>
>(
result: Result<R>, refs: [K, string][]
) {
return result.records.map((record) => {
let refsObj = refs.reduce((acc, [refProp, toProp]) => {
let key: string = record[refProp]; // no error now
let val = result.referredRecords[key];
return Object.assign(acc, { [toProp]: val });
}, {});
return Object.assign(record, refsObj);
});
}
Now R
ограничен не только to MyRecord
, но и to Record<K, string>
, поэтому любое свойство в key K
должно иметь тип string
. Этого достаточно, чтобы отключить эту ошибку в let key: string = ...
инструкции. Кроме того, K
(избыточно для нас, но не для компилятора) ограничено KeysMatching<T, string>
, что означает подмножество ключей T
, свойства которых являются string
значениями. Это помогает с IntelliSense при вызове функции.
РЕДАКТИРОВАТЬ: Как KeysMatching<T, V>
это работает … есть разные способы сделать это, но способ, описанный выше, заключается в использовании сопоставленного типа для перебора ключей K
T
и создания нового типа, свойства которого являются функцией свойства T
. -?
Модификатор делает каждое свойство нового типа обязательным, даже если свойство in T
является необязательным. В частности , я использовал условный тип , чтобы проверить T[K]
, совпадает ли V
он . Если это так, он возвращается K
; если нет, он возвращается never
. Итак, если мы это сделали KeysMatching<{a: string, b: number}, string>
, сопоставленный тип становится {a: "a", b: never}
. Затем мы индексируем этот сопоставленный тип с keyof T
помощью, чтобы получить типы значений. Так {a: "a", b: never}["a" | "b"]
становится "a" | never
то , что справедливо "a"
.
Давайте посмотрим на это в действии прямо сейчас:
interface Oops extends MyRecord {
foo: number;
bar: string;
}
declare const result: Result<Oops>;
mergeRecords(result, [["foo", "bar"]]); // error on "foo"
mergeRecords(result, [["bar", "baz"]]); // okay
Теперь первый вызов завершается с ошибкой, поскольку свойство at "foo"
не является a string
, в то время как второй вызов по-прежнему выполняется успешно, потому что свойство at "bar"
в порядке.
Ладно, надеюсь, это поможет. Удачи!
Комментарии:
1. Отличный ответ. Большое спасибо, очень тщательно, протестирую и приму как можно скорее. Если это сработает, я, вероятно, задам еще один вопрос о вводе возвращаемого значения, если вы думаете, что могли бы помочь и с этим.
2. Я понял большую часть этого, и это определенно работает, не могли бы вы подробнее объяснить тип сопоставления ключей? в частности, что означает часть ‘-?’ и [keyof T] в конце?