Typescript strict alias checking
Asked Answered
M

4

13

Is there a way to enforce typescript to even more strict typechecking up to per alias checks?

What I want to achieve is to define types, such as:

type kilograms = number;
type kilometers = number;
type kilogramsPerKilometer = number;

And to be sure that I don't put wrongly typed value into a variable such as:

let x: kilograms = 123;
let y: kilometers = 256;
let z: kilogramsPerKilometer = x / y; // Will popup an error here saying that types are incompatible

In which case it would require explicit type cast:

let x: kilograms = 123;
let y: kilometers = 256;
let z: kilogramsPerKilometer = <number>x / <number>y; // Will downcast `kilograms` and `kilometers` types to `number` and then up-cast types to `kilogramsPerKilometer`
Monikamoniker answered 22/1, 2017 at 11:37 Comment(0)
M
10

Somehow I lost track of this question while actually finding an interesting solution. It's prettier, doesn't affect interface and doesn't pollute the IDE's list of suggestions.

export type Miles = number & { readonly '': unique symbol };
export type Kilograms = number & { readonly '': unique symbol };
export type MilesPerKilogram = number & { readonly '': unique symbol };

var miles: Miles = 3; // error
var kilos: Kilograms = 3; // error
var milesPerKilos: MilesPerKilogram = x / y; // error

var x: Miles = 3 as Miles; // ok
var y: Kilograms = 3 as Kilograms; // ok
var u: MilesPerKilogram = x / y as MilesPerKilogram; // ok

miles = kilos; // error, yey!
kilos = miles; // error, yey!

Works on any types


Edit

Be careful as you are not allowed to extract { readonly '': unique symbol } part into separate type as it would share unique symbol across the types and make them share "nominality" which would beat the purpose of a nominal type:

export type Nominal<T> = T & { readonly '': unique symbol };

export type Miles = Nominal<number>;
export type Kilograms = Nominal<number>;
export type MilesPerKilogram = Nominal<number>;

var miles: Miles = 3; // error
var kilos: Kilograms = 3; // error
var milesPerKilos: MilesPerKilogram = x / y; // error

var x: Miles = 3 as Miles; // ok
var y: Kilograms = 3 as Kilograms; // ok
var u: MilesPerKilogram = x / y as MilesPerKilogram; // ok

miles = kilos; // ok, but it should be error! <================= This is due to declaration of Nominal<T>

Consider also https://basarat.gitbook.io/typescript/main-1/nominaltyping for more info

Monikamoniker answered 30/6, 2021 at 19:5 Comment(1)
Interesting trick, thank you for providing it ! :)Hellkite
G
4

There is no nominal typing in TS (yet) https://github.com/Microsoft/TypeScript/issues/202

This is the admitted solution (using discriminating unions):

interface kilograms {
  kind: "kilograms";
  value: number;
}

interface kilometers {
  kind: "kilometers";
  value: number;
}

function kilosPerKiloms(x: kilograms, y: kilometers): kilogramsPerKilometer {
  return x.value / y.value;
}

const x = { kind: "kilograms", value: 123 };
const y = { kind: "kilometers", value: 256 };
const z = kilosPerKiloms(x, y);
// const z = kilosPerKiloms(y, x); // => error

More informations there:

https://www.typescriptlang.org/docs/handbook/advanced-types.html

https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html

Gambia answered 10/5, 2017 at 9:21 Comment(0)
D
2

You are looking for nominal typing. Support is planned for future TypeScript versions (see Roadmap). For now, you have to use this: https://basarat.gitbooks.io/typescript/docs/tips/nominalTyping.html

Disorient answered 24/7, 2017 at 20:51 Comment(2)
Thank you for giving it a name! I was looking for exactly that.Sonatina
@Sonatina For more implementations, you can also look out for "branded types".Disorient
A
0

Here is a more generic version of @Lu4 answer:

type Brand<K, T> = K & { __brand: T }

type Miles = Brand<number, "Miles">;
type Kilograms = Brand<number, "Kilograms">;
type MilesPerKilogram = Brand<number, "MilesPerKilogram">;

let miles: Miles = 3; // error
let kilos: Kilograms = 3; // error
let milesPerKilos: MilesPerKilogram = x / y; // error

let x: Miles = 3 as Miles; // ok
let y: Kilograms = 3 as Kilograms; // ok
let u: MilesPerKilogram = x / y as MilesPerKilogram; // ok

miles = kilos; // error, yey!
kilos = miles; // error, yey!

The "brand" convention is used by the TypeScript team itself.

References:

Atilt answered 26/6 at 11:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.