Избегайте расширения при выводе универсальных типов

#typescript #type-inference #typescript-generics

#typescript #вывод типа #typescript-generics

Вопрос:

Возьмем этот пример функции

 type Decoder<A, B> = (v: A) => B 

declare function test<Values, D extends Decoder<Values, unknown>>(options: {
    values: Values,
    decoder: D,
    onDecoded: (decodedValue: ReturnType<D>) => unknown
}): void;
  

Идея заключается в том, что onDecoded получает на входе значение, вычисленное с помощью decoder . Однако:

 test({
    values: { a: "" },
    decoder: values => values.a.length,
    onDecoded: decodedValue => {
        decodedValue // unknown
    }
})
  

Как ни странно, если я не использую values в определении decoder , то decodedValue имеет правильный тип

 test({
    values: { a: "" },
    decoder: () => 42,
    onDecoded: decodedValue => {
        decodedValue // number
    }
})
  

Вот ссылка на игровую площадку с тем же примером

Есть ли способ заставить исходный пример работать?

Комментарии:

1. Что еще более странно (для меня), decodedValue тип меняется на number , если вы просто измените decoder на decoder: (values) => 42 . Простое объявление параметра, даже без его использования, меняет ситуацию.

2. Привет, спасибо за вопрос. Это правда, что D расширяется Decoder<Values, unknown> , но предполагается, что его фактический тип равен Decoder<Values, number> , поэтому я бы ожидал, что ReturnType<D> результатом будет number . Разве это не справедливое предположение?

3. Да, я все еще новичок в unknown типе, но, думаю, я понял. Спасибо.

4. на самом деле вы можете заменить там что угодно. unknown это просто разумная верхняя граница для этого случая, но в идеале я бы ожидал, что она никогда не будет выведена

5. и да, я тоже заметил такое поведение. Кажется, что простое добавление параметра изменяет алгоритм вывода типа…

Ответ №1:

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

Сначала позвольте мне преобразовать вашу подпись в почти эквивалентную версию, которая может быть более простой для анализа:

 declare function test<A, B>(options: {
    values: A,
    decoder: (a: A) => B,
    onDecoded: (b: B) => unknown
}): void;
  

Здесь возникает та же проблема с выводом, что и в вашей версии, но говорить о типах немного проще. В любом случае, компилятору необходимо вывести A и B из options значения passend, которое вы хотите вывести, A и B из него. Он может выводить A из типа values , но, вероятно, не сможет сделать вывод, B если только реализация decoder не зависит от A , поэтому он завершается неудачей.

Детали вывода типов — это не то, в чем я эксперт. Но если и существует канонический ответ на этот вопрос, то он находится в microsoft / TypeScript #38872, который использует очень похожую структуру данных и сталкивается с той же проблемой. Это классифицируется как ограничение дизайна в TypeScript, поэтому, вероятно, нет способа исправить это без изменения вашей test функции или способа, которым вы ее вызываете.


Изменение способа его вызова потребовало бы предоставления компилятору достаточной информации о типе, чтобы позволить ему работать. Например, если вы комментируете тип входного аргумента decoder при его вызове, все в порядке:

 test({
    values: { a: "" },
    decoder: (values: { a: string }) => values.a.length, // annotate
    onDecoded: decodedValues => {
        decodedValues // number
    }
})
  

Или вы можете изменить способ определения test() . Одно из моих предложений — разделить options объект на отдельные параметры. Компилятор немного охотнее тратит несколько проходов вывода для разных параметров функции, чем для одного параметра. Может быть, вот так:

 declare function test2<A, B>(values: A,
    decoder: (a: A) => B,
    onDecoded: (b: B) => unknown
): void;

test2(
    { a: "" },
    values => values.a.length,
    decodedValues => {
        decodedValues // number
    }
)

test2({ a: "" },
    () => 42,
    decodedValues => {
        decodedValues // number
    }
)
  

Эти выводы работают именно так, как вы хотите, и вы, вероятно, можете переписать их, используя D и ReturnType , если необходимо.


Я думаю, какой путь вы выберете, зависит от вас.

Ссылка на игровую площадку

Комментарии:

1. Спасибо за подробное объяснение и за указание на проблему с GitHub. На самом деле я получил очень похожий обходной путь (отдельные позиционные параметры), но теперь намного понятнее, почему это работает!