#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. На самом деле я получил очень похожий обходной путь (отдельные позиционные параметры), но теперь намного понятнее, почему это работает!