Тип TypeScript `NoInfer` не работает должным образом

У меня есть следующий код:

function inferTest<T>(factory: (setter: (setterFn: (arg: NoInfer<T>) => NoInfer<T>) => void) => T) {
    return factory(null!);
}
const inferred = inferTest((setter) => ({
    count: 0,
    increment: () => setter(state => ({ count: state.count + 1 })) // 'state' is of type 'unknown'.
}));
console.info(inferred.count); // 'inferred' is of type 'unknown'.

Я хочу, чтобы компилятор вывел тип T как

{
    count: number,
    increment: () => void,
}

Однако из-за наличия свойства increment в возвращаемом типе factory компилятор не может определить тип T и вместо этого назначает unknown в качестве типа. Это приводит к двум ошибкам компилятора:

'state' is of type 'unknown'.
'inferred' is of type 'unknown'.

Если вы удалите свойство increment, все будет работать нормально:

function inferTest<T>(factory: (setter: (setterFn: (arg: NoInfer<T>) => NoInfer<T>) => void) => T) {
    return factory(null!);
}
const inferred = inferTest(() => ({
    count: 0
}));
console.info(inferred.count);

Мне любопытно, почему использование аргумента setter в определении factory заставляет компилятор отказываться от вывода, особенно учитывая наличие типа NoInfer, используемого во всех проявлениях T, кроме одного. Кто-нибудь знает, что здесь происходит?

TS в любом случае не может сделать вывод из этих двух позиций, поэтому NoInfer не имеет никакого эффекта. Ваша проблема не имеет ничего общего с NoInfer, а связана с тем, что общий случай ms/TS#47599 нерешён и, вероятно, неразрешим в TS. У TS нет помощника PleaseInferThisFirstBeforeContextualTyping<T>, и просто отсутствие NoInfer<T> в этой позиции не делает этого возможным. Это полностью решает вопрос? Если да, то я напишу ответ; иначе чего не хватает?

jcalz 12.04.2024 19:04

@jcalz Да, это полностью решает вопрос. Я прочитаю все, что касается упомянутой вами проблемы GitHub. Не стесняйтесь написать полный ответ, когда у вас будет несколько циклов. Спасибо!

ChapterSevenSeeds 13.04.2024 15:16
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
2
247
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Утилита типа NoInfer<T> здесь ничего не делает и не предназначена для этой цели. Если вы пишете универсальную функцию, а TypeScript выводит аргумент типа из нежелательного места, вы можете использовать NoInfer, чтобы заблокировать этот вывод. Но в следующем коде:

function inferTest<T>(factory: (setter: (setterFn: (arg: T) => T) => void) => T) {
    return factory(null!);
}
const inferred = inferTest((setter) => ({
    count: 0,
    increment: () => setter(state => ({ count: state.count + 1 })) 
}));
// function inferTest<unknown>(
//   factory: (setter: (setterFn: (arg: unknown) => unknown) => void) => unknown
// ): unknown

вывод для T полностью терпит неудачу, и аргумент типа возвращается к неизвестному типу. Добавление NoInfer не изменит и не улучшит это.


Причина, по которой вывод не удался, заключается в том, что ваши обратные вызовы контекстно-зависимы . Вы не добавили аннотации setter или state. TypeScript имеет тенденцию откладывать вывод контекстно-зависимых функций до тех пор, пока не будет известен аргумент универсального типа. Но, конечно, этот аргумент типа зависит от типа функции. TypeScript считает это циклическим и не может продолжить. Вы надеетесь, что компилятор увидит только часть count возвращаемого типа, не зная о части increment. Но на самом деле это не так; TypeScript не знает, как выполнить «полное объединение », как описано в microsoft/TypeScript#30134. Вместо этого он использует эвристический алгоритм, который работает в самых разных ситуациях и особенно хорошо работает для наполовину написанного кода.

Общая проблема, связанная с невозможностью TypeScript выводить как контекстные типы, так и аргументы универсального типа, которые кажутся зависимыми друг от друга, описана в microsoft/TypeScript#47599 . Здесь были внесены улучшения, например, реализованные в microsoft/TypeScript#48538. Но продолжают быть и всегда будут необоснованные случаи. Я подозреваю, что причина, по которой ваша версия не работает так, как написано, заключается в том, что у вас есть вложенные контекстные обратные вызовы.

Возможно, ваш код будет поддерживаться как есть в какой-нибудь будущей версии TypeScript. Между тем, общий неутешительный совет заключается в том, что если вывод не делает то, что вам нравится, вручную аннотируйте и указывайте типы. Или проведите рефакторинг вашего кода так, чтобы вывод мог происходить не по кругу (иногда люди могут создавать построители, чтобы вместо одного самоссылающегося объекта они могли постепенно создавать его). Для вашего кода, вероятно, лучше всего просто определить тип для { count: number, increment: () => void } и вручную указать T в качестве этого типа:

interface MyType {
  count: number;
  increment(): void;
}
inferTest<MyType>((setter) => ({
  count: 0,
  increment: () => setter(state => (
    {
      count: state.count + 1,
      increment() { } // <-- you forgot this
    }))
}));

В любом случае NoInfer<T> ничего полезного в данной ситуации не сделает. Все, что он делает, это блокирует нежелательный вывод, который, возможно, может позволить произойти другому выводу. Но если вывода нет, то и блокировать нечего. Типа PleaseInfer<T> нет (и неясно, как такая вещь будет реализована таким образом, чтобы она действительно работала с microsoft/TypeScript#47599, а не просто терпела неудачу и там).

Ссылка на код детской площадки

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