Discriminación de tipos atómicos (tipos atómicos nominales) en TypeScript
Solo tengo curiosidad, ¿hay una manera de discriminar tipos atómicos para una mayor seguridad de tipos en TypeScript?
En otras palabras, ¿hay una manera de replicar el comportamiento siguiente:
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
O algo que permitiría ponerlo en un recipiente:
export type Discriminate<T> = ...
export type Kilos<T> = Discriminate<Kilos<T>>;
export type Pounds<T> = Discriminate<Pounds<T>>;
...
Editar
Ok, resulta que es posible construir ese tipo usando un truco de tipo imposible descubierto por ZpdDG4gta aquí https://github.com/microsoft/TypeScript/issues/202
Pero está un poco desordenado con la versión actual del idioma:
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
Desafortunadamente, lo siguiente no funcionaría:
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
Editar
Resulta que hay otra manera de introducir el tipo imposible, según @jcalz:
export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };
...
Sin embargo, todavía hay un problema con la falta de
export type Discriminator<T> = ...
¿Alguna idea para hacerlo más limpio? Dado que el alias de tipo hace que ambas referencias de tipo se adhieran a Discriminador...
Editar
Una mayor optimización mostró que es posible definir un tipo discriminado como:
export type Kilos<T> = T & { readonly '': unique symbol };
export type Pounds<T> = T & { readonly '': unique symbol };
Lo cual ayuda con la resolución de la contaminación de intellisense del IDE
Respuesta #1
Solo defínelo así:
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;
Entonces, Pounds y Kilos se convierten automáticamente en números y desde números, pero no entre ellos.
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
Si quieres prevenir la conversión automática de una unidad "mejorada" a un número "simple", entonces solo elimina 'opcional' del campo marcador:
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
