В следующем примере я не могу придумать ни одной ситуации, в которой присвоение Pick<Object, Key>Partial<Object> было бы неправильным, поэтому я ожидаю, что это будет разрешено.
Кто-нибудь может объяснить, почему это запрещено?
const fn = <T, K extends keyof T>(partial: Partial<T>, picked: Pick<T, K>) => {
/*
Type 'Pick<T, K>' is not assignable to type 'Partial<T>'.
Type 'keyof T' is not assignable to type 'K'.
'keyof T' is assignable to the constraint of type 'K', but 'K' could be instantiated with a different subtype of constraint 'string | number | symbol'.
*/
partial = picked;
};






@TitianCernicovaDragomir по сути прав в том, что компилятор обычно не может выполнять сложный анализ типов для неразрешенных универсальных типов. Это намного лучше с конкретными типами. См. Microsoft/TypeScript#28884 для обсуждения этого с Pick и Omit с дополнительными наборами ключей.
В этих ситуациях единственный способ продолжить — это лично убедиться, что задание правильное, а затем использовать утверждение типа, как в partial = picked as Partial<T>...
... но я бы не стал этого делать в данном случае. Ошибка здесь действительно хорошая, хотя трудно понять, почему, поскольку вы, по сути, только что перезаписали переменную partial и ничего не сделали с ней в области действия функции. Таким образом, несмотря на то, что код ненадежен, он безвреден, потому что ему не позволено сеять хаос где-либо еще. Давайте развяжем его, заставив fn() возвращать измененную partial переменную:
const fn = <T, K extends keyof T>(partial: Partial<T>, picked: Pick<T, K>) => {
partial = picked; // error, for good reason
return partial; // ?
};
Итак, основная проблема заключается в том, что Pick<T, K> является типом Шире, чем T. Он содержит свойства из T с ключами в K, но неизвестно, что нет содержит свойства с ключами нет в K. Я имею в виду, что значение типа Pick<{a: string, b: number}, "a"> вполне может иметь свойство b. И если он есть, он не обязательно должен быть типа number. Поэтому ошибочно присваивать значение типа Pick<T, K> переменной типа Partial<T>.
Давайте конкретизируем это на глупом примере. Представьте, что у вас есть интерфейс Tree и объект типа Tree, например:
interface Tree {
type: string;
age: number;
bark: string;
}
const tree: Tree = {
type: "Aspen",
age: 100,
bark: "smooth"
};
И у вас также есть интерфейс Dog и объект типа Dog, например:
interface Dog {
name: string;
age: number;
bark(): void;
}
const dog: Dog = {
name: "Spot",
age: 5,
bark() {
console.info("WOOF WOOF!");
}
};
Итак, dog и tree оба имеют числовое свойство age, и оба они имеют свойство bark разных типов. Один — это string, а другой — метод. Обратите внимание, что dog — это совершенно допустимое значение типа Pick<Tree, "age">, но значение неверный типа Partial<Tree>. И поэтому, когда вы звоните fn():
const partialTree = fn<Tree, "age">(tree, dog); // no error
мой модифицированный fn() возвращает dog как Partial<Tree>, и начинается веселье:
if (partialTree.bark) {
partialTree.bark.toUpperCase(); // okay at compile time
// at runtime "TypeError: partialTree.bark.toUpperCase is not a function"
}
Эта несостоятельность просочилась именно потому, что Pick<T, K>, как известно, не исключает или иным образом не ограничивает «не выбранные» свойства. Вы можете создать свой собственный StrictPicked<T, K>, в котором явно исключены свойства из T, которых нет в K:
type StrictPicked<T, K extends keyof T> = Pick<T, K> &
Partial<Record<Exclude<keyof T, K>, never>>;
И теперь ваш код более надежен (игнорируя странные вещи, такие как K, являющийся фирменным типом, как в комментарий выше)... но компилятор все еще не может это проверить:
const fn2 = <T, K extends keyof T>(
partial: Partial<T>,
picked: StrictPicked<T, K>
) => {
partial = picked; // also error
partial = picked as Partial<T>; // have to do this
return partial;
};
Это по-прежнему основной вопрос здесь; компилятор не может легко справиться с такими вещами. Может когда-нибудь будет? Но, по крайней мере, его не так легко неправильно использовать на стороне вызывающего абонента:
fn2<Tree, "age">(tree, dog); // error, dog is not a StrictPicked<Tree, "age">
Во всяком случае, надеюсь, что это поможет. Удачи!
Насколько ваш ответ имеет смысл, я все еще хожу по кругу, когда читаю сообщение об ошибке, предоставленное TypeScript: 'keyof T' is assignable to the constraint of type 'K', but 'K' could be instantiated with a different subtype of constraint 'string | number | symbol'. Было бы огромной помощью, если бы вы могли обновить свой ответ, чтобы разбить это сообщение об ошибке, чтобы я мог больше легко интерпретировать его в будущем? Я видел это во многих случаях, и каждый раз я оказывался в замешательстве.
Я почти уверен, что он смотрит на Pick<T, K> как на {[P in K]: T[P]} и Partial<T> как на {[P in keyof T]?: T[P]} и жалуется, что keyof T из Partial нельзя присвоить K из Pick. Если K уже, чем keyof T, вы рискуете получить проблему bark сверху.
Я мог бы отредактировать свой ответ позже, но сейчас я не в состоянии
Я обнаружил, что сопоставленные и условные типы обычно имеют странные ошибки, когда в них все еще есть неразрешенные параметры типа. Ошибка технически верна
Kextendskeyof Obejct, поэтому K может быть чем-то вродеkeyof Obejct & { brand: true }, хотя значение этого сомнительно.