Как сузить тип объединения TypeScript с помощью дженериков

У меня есть универсальная функция для обновления состояния (вариант использования - это динамическая обработка обновлений таблиц в React), и я использовал универсальные методы, чтобы гарантировать, что вызов функции является типобезопасным, но я не понимаю, почему TypeScript не компилируется.

В самой функции кажется, что Typescript не использует всю доступную информацию для сужения типа, а вместо этого думает, что я все еще работаю с полным объединением. Он явно знает достаточно об аргументах, чтобы определить, правильно ли вызывается функция, так почему же он терпит неудачу при проверке типов фактической реализации?

Ошибка на row[field] = value.

Минимальный пример:

type First = { a: string; b: string; c: string }
type Second = { b: string; c: string; d: string }
type Third = { c: string; d: string; e: string }

type State = {
  first: First[]
  second: Second[]
  third: Third[]
}

const update = <
  S extends State,
  TK extends keyof State,
  T extends S[TK],
  R extends T[number],
  F extends keyof R
>(
  state: State,
  tagName: TK,
  rowIndex: number,
  field: F,
  value: R[F]
) => {
  // fine
  const tag = state[tagName]

  // fine
  const row = tag[rowIndex]
  // keyof typeof row is now 'c'

  // TYPE ERROR
  row[field] = value
}

const state: State = {
  first: [{ a: "", b: "", c: "" }],
  second: [{ b: "", c: "", d: "" }],
  third: [{ c: "", d: "", e: "" }],
}

// this succeeds as expected
update(state, "first", 0, "a", "new")

// and this fails as expected
// @ts-expect-error
update(state, "second", 0, "a", "new")

площадка


person Sam Woolerton    schedule 11.03.2021    source источник


Ответы (1)


Попробуй это:

const update = <
  TK extends keyof State,
  F extends keyof State[TK][number]
>(
  state: State,
  tagName: TK,
  rowIndex: number,
  field: F,
  value: State[TK][number][F]
) => {
  // fine
  const tag = state[tagName]

  // fine
  // The type annotation here is required
  // (otherwise the type is inferred as First | Second | Third)
  const row: State[TK][number] = tag[rowIndex]

  // now also fine
  row[field] = value
}
person cherryblossom    schedule 11.03.2021
comment
Спасибо, работает отлично. Обратите внимание, что для этого требуется как двухэтапная const row: ... часть, так и изменение типа F, чтобы он не был просто keyof R. Понятия не имею, почему TS не может решить эту проблему, но хорошо иметь в виду - person Sam Woolerton; 12.03.2021