Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Define a type in typescript with conditional properties / limits

I'm new to typescript and have learned how to define a custom type like:

type T = {a:number, b:any}

Is it possible to define a type in TypeScript for the set of all strings with length larger than 2 using the type constructors defined in class?

or maybe define a type for the set of all numbers larger than 0?

like image 440
Ygrno Avatar asked Apr 05 '18 12:04

Ygrno


1 Answers

While you can't impose such arbitrary constraint at compile time, you can create types that force the user to call a functions that performs these validations and then rely on these invariants in your code using branded type

type PositiveNumber =  number & { positive: true}
type StringOfMinLength<T extends number> =  string & { minLegth: T}

type T = {a:PositiveNumber, b:StringOfMinLength<3> }

function isPositiveNumber(value: number): value is PositiveNumber {
    if( value < 0 ) return false
    return  true;
}
function asPositiveNumber(value: number) {
    if( !isPositiveNumber(value) ) throw new Error("Not ok")
    return value; // type guard above, value will now be a PositiveNumber 
}

function isStringOfMinLength<T extends number>(value: string, length: T): value is StringOfMinLength<T> {
    if( value.length < length ) return false;
    return true;
}

function asStringOfMinLength<T extends number>(value: string, length: T): StringOfMinLength<T> {
    if(!isStringOfMinLength(value, length) ) throw new Error("Not ok")
    return value; // type guard above, value will now be a PositiveNumber 
}

type MyData = {a:PositiveNumber, b:StringOfMinLength<3>}
let myObj: MyData = {
    a: asPositiveNumber(0),
    b: asStringOfMinLength("Test", 3),
}

Math.sqrt(myObj.a) // a will be greater then 0
myObj.b[2] // index will exist, length greater then 3

let myNotOkObject: MyData = {
    a: -1, // will be a compile error, the checking function is not called
    b: "Test" // this will also be an error event though it satisfies the constraint since we don't call the appropriate function
}

// we can also use the type guard version instead (is*) of the assertion version (as*)
let a = 10;
let b = "Test"
if(isPositiveNumber(a) && isStringOfMinLength(b, 3))
{
    let myOtherObj: MyData = { a, b } // a and b are PositiveNumber and respectively StringOfMinLength<3>
} else {
    // handle case when they are not what was expected
}

You can use a branded type anywhere the base type is required (ex Math.sqrt(myObj.a)) but you can't assign the base type directly to a field of the branded type. Whether this has value in real code is up to you and your use case.

This article has a bit more of a discussion on branded types.

Edit

Added a type guard version of the branded type creation functions, that way you can check if an invariant is true and handle the false case on your own instead of having an error thrown. 10x to @AluanHaddad for the idea.

like image 82
Titian Cernicova-Dragomir Avatar answered Sep 29 '22 14:09

Titian Cernicova-Dragomir