Атомная дискриминация типов (номинальные атомные типы) в TypeScript

Я просто любопытно, есть ли способ различать атомарные типы для большей безопасности типов в TypeScript?

Другими словами, есть ли способ повторить поведение ниже:

export type Kilos<T> = T & { discriminator: Kilos<T> };   // or something else  
export type Pounds<T> = T & { discriminator: Pounds<T> }; // or something else

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                  // Should give compiler error
wi.value = wi.value * 2;              // Shouldn't error, but it's ok if it would, because it would require type casting which asks for additional attention
wm.value = wi.value * 2;              // Already errors
const we: MetricWeight = { value: 0 } // Already errors

Или что-то, что позволило бы поместить его в один контейнер:

export type Discriminate<T> = ...

export type Kilos<T> = Discriminate<Kilos<T>>;
export type Pounds<T> = Discriminate<Pounds<T>>;

...

редактировать

Хорошо, оказывается, что можно создать такой тип, используя невозможный взлом типов, обнаруженный ZpdDG4gta здесь https://github.com/microsoft/TypeScript/issues/202

Но это немного грязно с текущей языковой версией:

export type Kilos<T> = T & { discriminator: any extends infer O | any ? O : never };
export type Pounds<T> = T & { discriminator: any extends infer O | any ? O : never };

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                       // Errors, good
wi.value = wi.value * 2;                   // Errors, but it's +/- ok
wi.value = wi.value * 2 as Pounds<number>; // Shouldn't error, good
wm.value = wi.value * 2;                   // Errors, good
const we: MetricWeight = { value: 0 }      // Errors, good

К сожалению, следующее не сработает:

export type Discriminator<T> = T & { discriminator: any extends infer O | any ? O : never } 

export type Kilos<T> = Discriminator<T>;
export type Pounds<T> = Discriminator<T>;

export interface MetricWeight {
    value: Kilos<number>
}

export interface ImperialWeight {
    value: Pounds<number>
}

const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }

wm.value = wi.value;                       // Doesn't error, this is bad
wi.value = wi.value * 2;                   // Errors, but it's +/- ok
wi.value = wi.value * 2 as Pounds<number>; // Shouldn't error, good
wm.value = wi.value * 2;                   // Errors, good
const we: MetricWeight = { value: 0 }      // Errors, good

редактировать

Оказывается, есть другой способ ввести невозможный тип, как в @jcalz:

export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };

...

Однако по-прежнему существует проблема с отсутствием

export type Discriminator<T> = ...

Есть мысли, чтобы сделать его чище? Так как псевдоним типа делает обе ссылки типа придерживаться Discriminator...

0 ответов

Просто определите это так:

const marker = Symbol();

export type Kilos = number & { [marker]?: 'kilos' };
export const Kilos = (value = 0) => value as Kilos;

export type Pounds = number & { [marker]?: 'pounds' };
export const Pounds = (value = 0) => value as Pounds;

Затем фунты и килограммы автоматически добавляются к числам и числам, но не друг к другу.

let kilos = Kilos(0);
let pounds = Pounds(0);
let wrong: Pounds = Kilos(20); // Error: Type 'Kilos' is not assignable to type 'Pounds'.

kilos = 10; // OK
pounds = 20;  // OK

let kilos2 = 20 as Kilos; // OK
let kilos3: Kilos = 30; // OK

pounds = kilos;  // Error: Type 'Kilos' is not assignable to type 'Pounds'.
kilos = pounds; // Error: Type 'Pounds' is not assignable to type 'Kilos'.

kilos = Kilos(pounds / 2); // OK
pounds = Pounds(kilos * 2); // OK

kilos = Pounds(pounds / 2); // Error: Type 'Pounds' is not assignable to type 'Kilos'.

kilos = pounds / 2; // OK
pounds = kilos * 2; // OK

Если вы хотите предотвратить автоматическое приведение от "расширенного" юнита к "простому" номеру, просто удалите опцию из поля маркера:

const marker = Symbol();
export type Kilos = number & { [marker]: 'kilos' };
// ------------------------------------^ -?
export const Kilos = (value = 0) => value as Kilos;

// then:
const kilos = Kilos(2); // OK
kilos = 2; // Error
kilos = kilos * 2; // Error
Другие вопросы по тегам