Почему тип Typescript, условный для `T extends undefined`, с экземпляром T, созданным с помощью` boolean`, разрешает T в `never`?

Следующий код пытается определить тип функции, которая вызывается без аргументов, если ее общий аргумент равен undefined, но с 1 аргументом для любого другого типа аргумента. (Возможно, есть более эффективные способы сделать это, ссылки на которые я бы хотел видеть в комментариях, но вопрос в том, почему Typescript работает не так, как я ожидал.)

Когда T extends undefined ложно, T превращается в never в ветке else, но только внутри списка параметров функции ...

type OptionalArgBroken<Arg> = Arg extends undefined ?
  () => void :
  (arg: Arg) => void;

const suppressArgBroken:OptionalArgBroken<undefined> = function() { };
suppressArgBroken(); // Fine

const haveArgBroken:OptionalArgBroken<boolean> = function(b:boolean) { };
haveArgBroken(true); // Type error

Как вы можете видеть на Playground, последняя строка выше дает ошибку типа

Аргумент типа "истина" не может быть назначен параметру типа "никогда". (2345)

Прочитав https://github.com/microsoft/TypeScript/issues/31751, Я попытался обернуть Arg и undefined в []s, и это, похоже, устранило проблему:

type OptionalArgWorks<Arg> = [Arg] extends [undefined] ?
  () => void :
  (arg: Arg) => void;

const suppressArgWorks:OptionalArgWorks<undefined> = function() { };
suppressArgWorks(); // Fine

const haveArgWorks:OptionalArgWorks<boolean> = function(b:boolean) { };
haveArgWorks(true); // Fine

Несмотря на то, что это исправление сработало, это не та проблема:

type MakesSenseT = undefined extends undefined ? 'yes' : 'no'
const MakesSense:MakesSenseT = 'yes';

type ExtendsUndefined<T> = T extends undefined ? 'yes' : 'no'

const MakesSenseToo : ExtendsUndefined<undefined> = 'yes';
const MakesSenseThree : ExtendsUndefined<boolean> = 'no';

Почему мой исходный код не работал?

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


person Jeffrey Yasskin    schedule 21.05.2020    source источник


Ответы (1)


Как написано,

type OptionalArgBroken<Arg> = Arg extends undefined ?
    () => void :
    (arg: Arg) => void;

является условным распределительным типом, поскольку тип checked, Arg, является параметром простого универсального типа.

«Распространенный» означает, что если переданный Arg является union, то тип будет оцениваться для каждого члена объединения отдельно, а затем снова объединяться вместе (так что операция распределяется по объединению). Другими словами, OptionalArgBroken<A | B | C> будет таким же, как OptionalArgBroken<A> | OptionalArgBroken<B> | OptionalArgBroken<C>.

Скорее всего, это не ваше намерение, о чем свидетельствует тот факт, что вы довольны результатами, когда завернули свой чек в [] (что делает проверенный тип больше не «обнаженным», «одевая» его).


Кроме того, компилятор TypeScript рассматривает тип boolean как сокращение для объединения true и false, так называемого логические типы литералов:

type Bool = true | false;
// type Bool = boolean

Если вы наведете курсор на Bool в своей среде IDE с IntelliSense, вы увидите, что Bool выше отображается как boolean.

Это может быть удивительно, если вы думаете о boolean как об одном типе, а не о объединении двух других типов. И одно место, где это проявляется, - это когда вы передаете boolean условному типу дистрибутива: OptionalArgBroken<boolean> это OptionalArgBroken<true | false>, что OptionalArgBroken<true> | OptionalArgBroken<false>, что является

type OABBool = OptionalArgBroken<boolean>;
// type OABBool = ((arg: false) => void) | ((arg: true) => void)

Вы передали то, что считали одним типом, и получили объединение типов функций. Ой. (См. microsoft / TypeScript # 37279)


А объединение типов функций можно безопасно вызывать только при пересечении их параметров. Прочтите Примечания к выпуску TS3.3 о поддержке вызова объединения функций, чтобы узнать, почему это так.

Но это означает, что значение типа OptionalArgBroken<boolean> может быть вызвано только с аргументом типа true & false, который уменьшается до never (см. microsoft / TypeScript # 31838), потому что не существует значения одновременно true и false.

И поэтому, когда вы пытаетесь вызвать haveArgBroken, он ожидает, что переданный параметр будет иметь тип never:

const haveArgBroken: OptionalArgBroken<boolean> = function (b: boolean) { };
// haveArgBroken(arg: never): void

И true не относится к типу never, поэтому он не работает:

haveArgBroken(true); // Type error

Вот почему ваш исходный код не работал.


Обратите внимание, что то же самое происходит с

type ExtendsUndefined<T> = T extends undefined ? 'yes' : 'no'

но это мягко, потому что ExtendsUndefined<boolean> становится ExtendsUndefined<true> | ExtendsUndefined<false>, который равен 'no' | 'no', который сокращается до всего 'no'. Это то, что вы хотите, но только потому, что нет способа отличить 'no', пришедший из true, от того, который пришел из false.


Хорошо, надеюсь, что это поможет; удачи!

площадка ссылку на код

person jcalz    schedule 21.05.2020