I'm just curios, is there a way to discriminate atomic types for greater type safety in TypeScript?
In other words, is there a way to replicate behavior below:
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
Or something that would allow to put it in one container:
export type Discriminate<T> = ...
export type Kilos<T> = Discriminate<Kilos<T>>;
export type Pounds<T> = Discriminate<Pounds<T>>;
...
Edit
Ok, it turns out it's possible to build such type using impossible type hack discovered by ZpdDG4gta here https://github.com/microsoft/TypeScript/issues/202
But it's a bit messy with current language version:
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
Unfortunately the following wouldn't work:
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
Edit
It turns out that there is another way to introduce the impossible type, as per @jcalz:
export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };
...
However there's still an issue with the lack of
export type Discriminator<T> = ...
Any thoughts to make it cleaner? Since type aliasing makes both type references stick to Discriminator...
Edit
Further optimization shown that it's possible to define discriminated type as:
export type Kilos<T> = T & { readonly '': unique symbol };
export type Pounds<T> = T & { readonly '': unique symbol };
Which helps with resolution of IDE's intellisense pollution