Define TypeScript type using a function that tests the value

Viewed 31

Exactly as it says in the title. Example would be defining a type Integer such that x only qualifies as Integer if isInteger(x) returns true.

2 Answers

Typescript doesn't do this type of thing. Typescript facilitates a layer of design-time tooling that serves as guard-rails to prevent the authoring of code that is not likely to work. What you're asking for is a runtime check.

Typescript cannot differentiate an integer from a float. You have number which is any floating point number, and you have finite unions of number literals like 1 | 2 | 3. That's all you get.


However, one way to emulate this is with branded types. Branding lets you stamp a bit of metadata on a type that make it different that than the type receiving the stamp. This lets you write a type predicate that verifies somethings at runtime, and if it passes then is considered to bear that brand.

For example:

const integerBrand = Symbol('integerBrand')
type Integer = number & { [integerBrand]: true }

function isInteger(x: number): x is Integer {
    return x === Math.floor(x)
}

Here we first create a symbol to use a key for the brand. We use a symbol because that means that key will be globally unique and guaranteed not to clash with any type that this brand will be added to.

Then we create a type called Integer which is an intersection of number and an object with the symbol as it's only property. This branding object never exists at runtime, it just to make typescript acknowledge this type as a special number.

Lastly, we create a type predicate function called isInteger that tests a number and casts it to Integer if the test was successful.

Now we can create a function that only accepts Integer arguments:

function addInts(a: Integer, b: Integer): Integer {
    return (a + b) as Integer
}

And some numbers to play with:

// maybe integers, maybe not.
const a = Math.random() > 0.5 ? 1 : 3.1459
const b = Math.random() > 0.5 ? 2 : 1.6180

Now if you pass these numbers to that, you get a type error:

addInts(a, b) // type error
// Argument of type 'number' is not assignable to parameter of type 'Integer'.

But if you check them with isInteger first it will work.

if (isInteger(a) && isInteger(b)) {
    addInts(a, b) // fine
}

The downside of this approach is that the brand must be applied by your functions exclusively, which can be a verbose pain.


You could also have a createInt function to make Integers for you.

function createInt(x: number): Integer {
  if (isInteger(x)) return x
  throw new Error("x is not an integer value")
}

See playground

Related