Компилятор TS недоволен типом объединения, включающим бренд перечисления (номинальная типизация)

Я использую перечисление для достижения номинальной типизации (как, например, предложено в TypeScript Deep Dive книга):

enum ExampleIdBrand {}
export type ExampleId = ExampleIdBrand & string
const exampleId: ExampleId = '42' as ExampleId

const m1 = (e: ExampleId) => e.toUpperCase()
m1(exampleId) // ✅

Пока все работает так, как и ожидалось. Однако, если я изменю метод, чтобы принять (более широкий) тип объединения, компилятор больше не принимает мой exampleId:

const m2 = (e: ExampleId | 'whatever') => e.toUpperCase()
m2('whatever') // ✅
m2(exampleId) // ???? Does not compile

Почему последняя строка не компилируется? (ТУ 3.3.4000)


person Rahel Lüthy    schedule 05.04.2019    source источник
comment
Привет, Рахель, ты говоришь на каком-то особом диалекте? .toUpperCase() — это функция, для нее требуются фигурные скобки.   -  person Daniel Dietrich    schedule 05.04.2019
comment
Никакого особого диалекта, просто я неаккуратный - исправлю, тс!   -  person Rahel Lüthy    schedule 05.04.2019
comment
@DanielDietrich, это тоже будет работать .. просто делает что-то другое ..   -  person Titian Cernicova-Dragomir    schedule 05.04.2019
comment
Да, я немного повозился. Актерский состав в const exampleId: ExampleId = '42' as ExampleId; уже запах. Если мы удалим приведение и сделаем ExampleId строкой export type ExampleId = string;, это сработает. Кажется, что невозможно создать типы пересечения ~~union~~ с перечислениями. Немного погуглю...   -  person Daniel Dietrich    schedule 05.04.2019
comment
Ну, но тогда мы теряем номинальную типизацию (netzwerg.ch /blog/2018/11/21/react-redux-typescript/#5)   -  person Rahel Lüthy    schedule 05.04.2019


Ответы (2)


В TypeScript перечисления могут иметь числовой или строковый тип. У числа нет метода .toUpperCase().

Ваш пример должен работать, потому что перечисление сужено до строки типа.

Обходной путь:

enum ExampleIdBrand {}
export type ExampleId = ExampleIdBrand;
const exampleId: ExampleId = '42';

const m1 = (e: ExampleId) => e.toString().toUpperCase();
m1(exampleId);

const m2 = (e: ExampleId | 'whatever') => e.toString().toUpperCase();
m2('whatever'); // ✅
m2(exampleId); // ✅
person Daniel Dietrich    schedule 05.04.2019
comment
Однако ты права, Рахель. После глубокого погружения в TypeScript я также ожидаю, что ExampleId & string сужает перечисление до строкового типа. Ваш пример должен компилироваться, но это не так. Может баг компилятора? В моем примере выше союз вообще не нужен. Я отредактирую пост... - person Daniel Dietrich; 05.04.2019
comment
Это не то, что пытается сделать пользователь, они хотят создать тип, который, хотя строка во время выполнения несовместима ни с какой другой строкой во время компиляции, по крайней мере, без утверждения типа. stackoverflow.com/questions/ 49557714/ - person Titian Cernicova-Dragomir; 05.04.2019
comment
Это, безусловно, звучит как хороший обходной путь, спасибо @DanielDietrich. Но так же, как и вы, я до сих пор не понимаю, почему мой первоначальный пример не компилируется ???? - person Rahel Lüthy; 05.04.2019

То, что происходит с пересечением, являющимся пустым множеством (т. е. нет значений, которые могут быть экземплярами пересечения), — это то, что изменилось. Я не могу найти документы, хотя я буду продолжать искать, но при определенных условиях такие пересечения рухнут в никогда. И мы видим, как это работает в данном случае, ExampleId | 'whatever' = never | 'whatever' = 'whatever'

const m2 = (e: ExampleId | 'whatever') => e.toUpperCase()
type x =  Parameters<typeof m2>[0] //  'whatever'

Чтобы сохранить номинальный характер ExampleId, мы можем вместо этого добавить свойство:

enum ExampleIdBrand {}
export type ExampleId = { __brand: ExampleIdBrand } & string
const exampleId: ExampleId = '42' as ExampleId

const m1 = (e: ExampleId | "whatever") => e.toUpperCase()
m1(exampleId) // ✅
m1("whatever")// ✅

Или, если мы хотим действительно хорошо скрыть этот член, мы можем использовать пересечение с классом с закрытым полем:

enum ExampleIdBrand { }
class Brand<T> { private __brand: T}
export type ExampleId = Brand<ExampleIdBrand> & string
const exampleId: ExampleId = '42' as ExampleId

const m1 = (e: ExampleId | "whatever") => e.toUpperCase()
m1(exampleId) // ✅
m1("whatever")// ✅

Или отбросьте перечисление и используйте это class ExampleIdBrand { private __brand!: any}

person Titian Cernicova-Dragomir    schedule 05.04.2019
comment
Большое спасибо за ваши предложения. Что происходит с перекрестком, который на самом деле нельзя создать — не могли бы вы пояснить, почему этот перекресток нельзя создать? - person Rahel Lüthy; 05.04.2019
comment
@RahelLüthy немного уточнил, используя установленный язык. Я думаю, что это соответствующие PR: github.com/Microsoft/TypeScript/issues/20001, github.com/Microsoft/TypeScript/pull/23751 - person Titian Cernicova-Dragomir; 05.04.2019
comment
Ничего себе, PR 23751, кажется, объясняет это: 10 | число уменьшено до 10 — это НАСТОЛЬКО не то, как я ожидал, что это сработает ???? Вы знаете об этом, @DanielDietrich? - person Rahel Lüthy; 05.04.2019
comment
@RahelLüthy, это опечатка, 10 | number это number. 10 & number это 10 - person Titian Cernicova-Dragomir; 05.04.2019
comment
Это, безусловно, имело бы больше смысла! - person Rahel Lüthy; 05.04.2019