Typescript, с типами R и K расширяет keyof R, как объявить, что R[K] имеет тип string

#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] в конце?