TypeScript: назначение `Pick<T, K>` для `Partial<T>`

В следующем примере я не могу придумать ни одной ситуации, в которой присвоение 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;
};

Пример игровой площадки TypeScript

Я обнаружил, что сопоставленные и условные типы обычно имеют странные ошибки, когда в них все еще есть неразрешенные параметры типа. Ошибка технически верна K extends keyof Obejct, поэтому K может быть чем-то вроде keyof Obejct & { brand: true }, хотя значение этого сомнительно.

Titian Cernicova-Dragomir 27.06.2019 12:24
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
1
3 486
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

@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'. Было бы огромной помощью, если бы вы могли обновить свой ответ, чтобы разбить это сообщение об ошибке, чтобы я мог больше легко интерпретировать его в будущем? Я видел это во многих случаях, и каждый раз я оказывался в замешательстве.

Oliver Joseph Ash 27.06.2019 21:17

Я почти уверен, что он смотрит на 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 сверху.

jcalz 27.06.2019 21:30

Я мог бы отредактировать свой ответ позже, но сейчас я не в состоянии

jcalz 27.06.2019 21:31

Другие вопросы по теме