Слишком широкий тип ключа индекса TypeScript

#typescript #generics #keyof

#typescript #ключ

Вопрос:

Извиняюсь, я уверен, что на этот вопрос где-то был дан ответ, но я не уверен, что искать в Google. Пожалуйста, отредактируйте мой вопрос, если термины в названии неверны.

У меня есть что-то вроде этого:

 type RowData = Record<string, unknown> amp; {id: string}; 


type Column<T extends RowData, K extends keyof T> = {  
  key: K; 
  action: (value: T[K], rowData: T) => void;
}

type RowsAndColumns<T extends RowData> = {
  rows: Array<T>; 
  columns: Array<Column<T, keyof T>>; 
}
  

И TypeScript должен иметь возможность определять типы action функций, изучая форму строк и то, какое key значение было присвоено столбцу:

ie:

 function myFn<T extends RowData>(config: RowsAndColumns<T>) {

}

myFn({
  rows: [
    {id: "foo", 
      bar: "bar", 
      bing: "bing", 
      bong: {
        a: 99, 
        b: "aaa"
      }
    }
  ], 
  columns: [
    {
      key: "bar", 
      action: (value, rowData) => {
          console.log(value);
      }
    },
     {
      key: "bong", 
      action: (value, rowData) => {
          console.log(value.a); //Property 'a' does not exist on type 'string | { a: number; b: string; }'.

      }
    }
  ]
}); 

  

Ссылка на игровую площадку

Проблема в том, что TypeScript, похоже, выводит тип value ( value: T[K] ) как «типы всех, доступных для всех ключей T», а не использует только ключ, указанный в объекте столбца.

Почему TypeScript делает это и как я могу это решить?

Что могло бы дать хороший ответ, так это определение некоторых конкретных терминов и понятий.

Я полагаю, я хочу изменить свой K extends keyof T на что-то вроде «K — это ключ T, но только один ключ, и он никогда не меняется».

Комментарии:

1. Не могу проверить это прямо сейчас, но key: "bar" as const вместо того, чтобы просто key: "bar" помочь?

2. @Cerberus нет — я пробовал это 🙂

Ответ №1:

Если вы ожидаете, что ключи of T будут объединением литералов типа like "bong" | "bing" | ... (и не только string ), тогда вы можете выразить тип, который сам по себе является объединением Column<T, K> для каждого ключа K keyof T .

Обычно я делаю это с помощью немедленной индексации (поиска) в сопоставленный тип:

 type SomeColumn<T extends RowData> = {
  [K in keyof T]-?: Column<T, K>
}[keyof T]
  

но вы также можете сделать это с помощью распределительных условных типов:

 type SomeColumn<T extends RowData> = keyof T extends infer K ?
  K extends keyof T ? Column<T, K> : never : never;
  

В любом случае, ваш RowsAndColumns тип будет использовать SomeColumn вместо Column :

 type RowsAndColumns<T extends RowData> = {
  rows: Array<T>;
  columns: Array<SomeColumn<T>>;
}
  

И это позволяет вашему желаемому варианту использования работать так, как ожидалось, без ошибок компиляции:

 myFn({
  rows: [
    {
      id: "foo",
      bar: "bar",
      bing: "bing",
      bong: {
        a: 99,
        b: "aaa"
      }
    }
  ],
  columns: [
    {
      key: "bar",
      action: (value, rowData) => {
        console.log(value);
      }
    },
    {
      key: "bong",
      action: (value, rowData) => {
        console.log(value.a);
      }
    },
  ]
});
  

Игровая площадка ссылка на код

Комментарии:

1. Большое вам спасибо! Что это -? за синтаксис — я никогда этого не видел.

2. Он удаляет необязательный модификатор из свойств отображаемого типа (в противном случае вы получаете странные undefined s в типах значений); см. Эту документацию

3. Ваши ответы потрясающие, большое вам спасибо! :3

4. комментарий, чтобы сказать, что это не работает с TS 4.8.4

5. @AbeCaymo Я только что посмотрел и не вижу никакой очевидной проблемы; не могли бы вы уточнить, что конкретно «не работает»?