Тип индекса TypeScript слишком широкий

Nov 08 2020

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

У меня примерно так:

type RowData = Record<string, unknown> & {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значение было присвоено столбцу:

то есть:

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, но только одна клавиша, и она никогда не меняется».

Ответы

5 jcalz Nov 09 2020 at 14:21

Если вы ожидаете , что ключи Tбыть объединением из литералов , как "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);
      }
    },
  ]
});

Ссылка для игровой площадки на код