Мне еще предстоит выяснить, почему ваш тип Opaque<T, U>
неправильно сужается с помощью анализа потока управления при использовании операторов if
/ else
или switch
/ case
. Когда вы передаете примитивный тип данных для T
в Opaque<T, U>
, как в Animal
с "Cat" | "Dog"
, вы получаете то, что называется "фирменным примитивом", как упомянуто в этой записи FAQ о создании номинальных типов . Кажется, что происходит, когда у вас есть фирменный примитив val
и вы используете обычную проверку типа для другого примитива somePrimitive
, такого как if (val === somePrimitive) { /*true*/ } else { /*false*/ }
или switch (val) { case somePrimitive: /* true */ break; /*false*/
}, в «истинной» части все в порядке проверки: тип val
сужается до чего-то вроде Extract<
typeof val, typeof somePrimitive>
. Так что в вашем случае if (animal === "Dog") { /*true*/ } else { /*false*/ }
сужается до Opaque<"Dog", "Animal">
в истинной ветви.
То, что не отлично, это то, что происходит в «ложной» части проверки. Если val
равно , а не равно somePrimitive
, тогда мы сможем сузить его до Exclude<
typeof val, typeof somePrimitive>
. То есть, когда animal
не равно "Dog"
, компилятор должен сузить animal
до Opaque<"Cat", "Animal">
. Но этого не происходит.
Иногда в таких проверках правильно не сужаться в ложной ветви. Например, когда ваши типы не являются синглетонами и могут иметь более одного допустимого значения этого типа. Если бы у меня было function f(x: string | number) { if (x === "Dog") { /*true*/ } else { /*false* } }
, имеет смысл сузить x
до string
(или даже "Dog"
) в истинной ветви, но вы бы не хотели сужать x
до number
в ложной ветви , Самое безопасное, что нужно сделать, когда компилятор не знает точно, что происходит, - это сузить в истинной ветви и не сузить в ложной ветви.
Но я не ожидал, что компилятор примет это маршрут в случае фирменного примитива. Тип animal
не может быть Opaque<"Dog", "Animal">
, если у вас есть animal !== "Dog"
. Так что я склонен подать проблему GitHub по этому поводу и посмотреть, что они говорят; Это похоже на ошибку или, по крайней мере, ограничение дизайна. Я немного удивлен, что не встречал такого ранее, и что не могу найти прямо соответствующую проблему, уже поданную. Ну хорошо.
Итак, какие обходные пути возможны? Одним из них является создание определяемой пользователем функции защиты типа . Определяемые пользователем средства защиты типов обычно обрабатываются компилятором так, что даже результат false
подразумевает сужение параметра. Это не всегда желательно (см. microsoft / # 15048 для предложения о том, чтобы такие функции защиты типов были более конфигурируемыми, чтобы возврат false
не был сужен), но это то, что вам нужно здесь. Это может быть реализовано так:
function is<T extends string>(x: any, v: T): x is T {
return x === v;
}
function makeSound(animal: Animal) {
if (is(animal, "Dog")) {
return "Haw!";
} else if (is(animal, "Cat")) {
return "Meow"!
} else {
assertNever(animal); // no error now
}
}
Это работает. Конечно, как вы упомянули, требуется рефакторинг всех ваших операторов switch
/ case
в вызовы функций if
/ else
, так что это может быть слишком болезненным.
В идеале TypeScript должен поддерживать более официальный непрозрачный / номинальный тип, такой как предложение бренда типа unique
в microsoft / TypeScript # 33038 . Но пока самый простой обходной путь, который я могу здесь представить, который позволяет вам сохранить ваши операторы switch
, состоит в использовании string enum .
Обычно я вообще не рекомендую использовать перечисления, поскольку они имеют странные предостережения и не соответствуют текущей методологии разработки TypeScript (перечисления - это функциональные возможности времени выполнения, отсутствующие в чистом JavaScript, идущий вразрез с нецелевой # 6) ... но, по крайней мере, они ведут себя как положено, когда используются в качестве дискриминанта:
enum Animal {
DOG = "Dog",
CAT = "Cat"
}
function makeSound(animal: Animal) {
switch (animal) {
case Animal.DOG:
return "Woof!"; // English-speaking dog
case Animal.CAT:
return "Meow!";
default:
assertNever(animal); // no error
}
}
Здесь ваши Opaque
, animals
и Animal
заменяются одним Animal
перечислением. Обратите внимание, что в makeSound
мы должны тестировать против Animal.DOG
и Animal.CAT
вместо "Dog"
и "Cat"
. В противном случае компилятор по-прежнему не будет сужать ложный случай. К счастью, проверка значений enum работает .
Итак, это мои мысли. Надеюсь, они помогут вам продолжить. Удачи!
Детская площадка ссылка на код