Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make unique types from basic types in TypeScript?

Tags:

typescript

I want to give class objects unique id types, even though they're all strings. I tried using type and I tried deriving from a base class with unique subclass names.

See the following example. Neither type nor extends allows me to instruct the compiler to treat these as unique types. I can still pass a HumanId to a function expecting an AnimalId and visa versa.

I get that they're object compatible, and that from the underlying JavaScript perspective, this makes total sense. In fact, if I add a unique member to AnimalId, I get the error I expect:

Argument of type 'HumanId' is not assignable to parameter of type 'AnimalId'.

Is there a good approach with TypeScript to make unique type aliases for basic types?

// type HumanId = string;
// type AnimalId = string;

class id {
    constructor(public value: string) { }
    toString(): string { return this.value;}
}
class HumanId extends id { };
class AnimalId extends id { };

function humanTest(id: HumanId): void {

}

function animalTest(id: AnimalId): void {

}

let h: HumanId = new HumanId("1");
let a: AnimalId = new AnimalId("2");

animalTest(h);
like image 541
Nimai Avatar asked Jul 15 '17 00:07

Nimai


People also ask

Is it possible to combine types in TS?

An intersection type combines multiple types into one. This allows you to add together existing types to get a single type that has all the features you need.

How do you declare a variable with two types TypeScript?

In TypeScript, a union type variable is a variable which can store multiple type of values (i.e. number, string etc). A union type allows us to define a variable with multiple types. The union type variables are defined using the pipe ( '|' ) symbol between the types. The union types help in some special situations.


1 Answers

I came across this question, but my use case had a minor twist: I wanted to introducing a unique type for number. Think of an API where you have e.g. hours: number, minutes: number, seconds: number, etc. but you want the type system to enforce correct usage of all units.

The blog post mentioned by @Evert is a great resource in that regard. The idea is to create an intersection type with some dummy that is never actually used. Creating new unique types can be abstracted away by a generic helper type. Illustrating on the example:

// Generic definition somewhere in utils
type Distinct<T, DistinctName> = T & { __TYPE__: DistinctName };

// Possible usages
type Hours = Distinct<number, "Hours">;
type Minutes = Distinct<number, "Minutes">;
type Seconds = Distinct<number, "Seconds">;

function validateHours(x: number): Hours | undefined {
  if (x >= 0 && x <= 23) return x as Hours;
}
function validateMinutes(x: number): Minutes | undefined {
  if (x >= 0 && x <= 59) return x as Minutes;
}
function validateSeconds(x: number): Seconds | undefined {
  if (x >= 0 && x <= 59) return x as Seconds;
}

Now a function f(h: Hours, m: Minutes, s: Seconds) cannot be called with just any number, but ensures full type safety. Also note that the solution has no memory/runtime overhead.

In practice this approach works well for me, because these "distinct" types can be used implicitly in places where number is required. Explicit conversion via e.g. as Hour is only necessary the other way around. A minor drawback is that expressions like hours += 1 need to be replaced by hours = (hours + 1) as Hours. As demonstrated in the blog post, the benefits can often outweigh the slightly more explicit syntax though.

Side note: I have called my generic type Distinct because the name feels more natural to me, and this is how the feature is called in the Nim programming language.

like image 62
bluenote10 Avatar answered Nov 11 '22 03:11

bluenote10