Тип '1' нельзя назначить типу 'T [Extract]» - PullRequest
1 голос
/ 07 июля 2019

У меня есть следующая функция, которая принимает параметр типа T, который расширяет Foo (который является объектом). В функции он выполняет итерацию по каждому ключу данного объекта, чтобы создать новый объект, имеющий точно такие же ключи, но все соответствующие значения равны 1 (что эта функция на самом деле не имеет значения).

Но он не компилируется с Type '1' is not assignable to type 'T[Extract<keyof T, string>]'.. Я думал, что T[Extract<keyof T, string>] - это number, поэтому назначение 1, то есть number, должно работать.

Что не так в моем коде?

type Foo = {
  [key: string]: number
}

const func = <T extends Foo>(obj: T): T => {
  for (const name in obj) {
    obj[name] = 1
  }
  return obj
} 

1 Ответ

1 голос
/ 08 июля 2019

Компилятор, как правило, не выполняет очень сложный анализ операций с универсальными типами (то есть типами, которые зависят от параметра неразрешенного типа, такого как T внутри реализации func()) ... он обычно лучше работа с конкретными типами (например, Foo), которые более просты.

Таким образом, компилятор полностью доволен и разрешит следующую конкретную версию вашей функции:

const concreteFunc = (obj: Foo): Foo => {
  for (const name in obj) {
    obj[name] = 1; // okay
  }
  return obj; // okay
};

Поскольку неразрешенный универсальный тип еще не известен, компилятор будет менее уверен в том, что то, что вы делаете, безопасно, и может выдать предупреждение. Это предупреждение не обязательно означает, что вы определенно допустили ошибку.

Такая ситуация часто происходит внутри реализации обобщенных функций. Если вы тщательно проанализируете, что делаете, и решите, что это действительно безопасная от типа, вы можете использовать утверждения типа для удаления предупреждения.

Например, вы можете сделать это:

const func = <T extends Foo>(obj: T): T => {
  for (const name in obj) {
    obj[name] = 1 as T[typeof name]; // assert BUT BEWARE ☠
  }
  return obj;
};

Но учтите, что утверждение типа означает, что ответственность за безопасность типов перешла от компилятора к вам ... и (для ответа на ваш вопрос) это небезопасно .

И вот почему ... рассмотрим следующий код:

interface Bar extends Foo {
  two: 2;
  four: 4;
  six: 6;
  eight: 8;
}

const bar: Bar = {
  two: 2,
  four: 4,
  six: 6,
  eight: 8
};

const b = func(bar);

console.log(b.two); // 2 at compile time, but prints 1!
console.log(b.four); // 4 at compile time, but prints 1!
console.log(b.six); // 4 at compile time, but prints 1!
console.log(b.eight); // 4 at compile time, but prints 1!

Здесь мы видим интерфейс Bar, который расширяет Foo путем добавления известных свойств, значения которых числовые литералы , ни одно из которых не равно 1. Когда мы вызываем func(bar), T определяется как Bar, и, следовательно, выход func(bar) также должен быть Bar.

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

Так что, вероятно, вам не следует использовать утверждения в такой функции, как func(). Там может быть действительно безопасный способ написать func() ... например, возможно, это:

const funcSafer = <
  T extends { [K in keyof T]: 1 extends T[K] ? unknown : never }
>(
  obj: T
): T => {
  for (const name in obj) {
    obj[name] = 1 // error! still need "as T[typeof name]"
  }
  return obj;
};

Здесь ограничение на T заключается в том, что 1 должен быть назначен всем его свойствам. Это имеет следующие желательные эффекты:

funcSafer(bar); // error! property "two" is incompatible
const foo: Foo = {two: 2, four: 4}; // just Foo, not Bar
funcSafer(foo); // okay
funcSafer({a: 1 as 1}); // okay
funcSafer({a: 4}); // okay, interpreted as {a: number}
funcSafer({a: 4 as 4}); // error, "a" is incompatible

Но, конечно, компилятор все еще не может сказать, что obj[name] = 1 безопасен внутри реализации. Это слишком сложно ... поэтому нам нужно утверждение.

Хорошо, надеюсь, это поможет. Удачи!

Ссылка на код

...