Rajab Shakirov
Rajab Shakirov

Reputation: 8035

GUID / UUID type in typescript

I have this function:

function getProduct(id: string){    
    //return some product 
}

where id is actually GUID. Typescript doesn't have guid type. Is it possible create type GUID manually?

function getProduct(id: GUID){    
    //return some product 
}

so if instead 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' will be some 'notGuidbutJustString' then I will see typescript compilation error.

Update: as David Sherret said: there is no way to ensure a string value based on regex or some other function at compile time but it is possible do all the checks in one place at run time.

Upvotes: 48

Views: 83407

Answers (5)

plumpNation
plumpNation

Reputation: 162

You can use template literals now

export type UUID = `${string}-${string}-${string}-${string}-${string}`;

for instance. It won't validate your code on the way into the system, but does provide a more specific string type. The same can be used for all sorts of string based types, like date iso strings. It's not incredibly accurate, as you can see it could allow a-b-c-3-f.

Upvotes: 3

Benoit Ranque
Benoit Ranque

Reputation: 114

Adding my answer here, which is built on the answers above:

// use a brand to create a tagged type. Horrible hack but best we can do
export type UUID = string & { __uuid: void };

// uuid regex
const UUID_REGEX = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;

// type guard to assert a string is a valid uuid
export function isUUID(uuid: string): uuid is UUID {
  return UUID_REGEX.test(uuid);
}

The trick is to use typescript type guards to assert a string is a valid UUID.

Unlike a simple type UUID = string type alias, typescript won't silently coerce strings to UUID.

You'll need to check a string is a valid UUID before using it where a UUID is expected.

Here's an example:

function needUUID(uuid: UUID) {
  console.log(uuid)
}

const input = ''

// this won't compile, we don't know whether input is a valid UUID
needUUID(input)

if (isUUID(input) {
  // this compiles successfully, we've verified that input is a valid UUID
  needUUID(input)
} else {
  // this fails to compile, we know input is _not_ a valid uuid
  needUUID(input)
}

Upvotes: 2

Johannes P.
Johannes P.

Reputation: 4133

I really like @DavidSherret's updated version using the idiomatic approach for strongly typed primitives, namely via a branded type / tagged union type (+1).

Expanding upon it by adding a type parameter for the brand, one can even tie the ID to a specific entity or object type (like the "Product" in the OP's question):

type OptionalRecord = Record<string, unknown> | undefined

type Uuid<T extends OptionalRecord = undefined> = string & { __uuidBrand: T }

type Product = {
    id: Uuid<Product>
    name: string
}

type ProductId = Product['id']

function uuid<T extends OptionalRecord = undefined>(value: string) {
    return value as Uuid<T>
}

function productId(value: string) {
    return uuid<Product>(value)
}

function funcWithProductIdArg(productId: ProductId) {
    // do something
    return productId
}

const concreteProductId = productId('123e4567-e89b-12d3-a456-426614174000')

// compiles
funcWithProductIdArg(concreteProductId)

// Argument of type 'string' is not assignable to parameter of type 'ProductId'.
//  Type 'string' is not assignable to type '{ __uuidBrand: Product; }'.(2345)
//
// @ts-expect-error Not a ProductId.
funcWithProductIdArg('123e4567-e89b-12d3-a456-426614174000')

TypeScript Playground

Upvotes: 1

Stefan Steiger
Stefan Steiger

Reputation: 82306

I think one should extend a bit on the answer by David Sherret.
Like this:

// export 
class InvalidUuidError extends Error {
    constructor(m?: string) {
        super(m || "Error: invalid UUID !");

        // Set the prototype explicitly.
        Object.setPrototypeOf(this, InvalidUuidError.prototype);
    }

}


// export 
class UUID 
{
    protected m_str: string;

    constructor(str?: string) {
        this.m_str = str || UUID.newUuid().toString();

        let reg:RegExp = new RegExp("[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}", "i")
        if(!reg.test(this.m_str))
            throw new InvalidUuidError();
    }

    toString() {
        return this.m_str;
    }

    public static newUuid(version?:number) :UUID
    {
        version = version || 4;


        // your favourite guid generation function could go here
        // ex: http://stackoverflow.com/a/8809472/188246
        let d = new Date().getTime();
        if (window.performance && typeof window.performance.now === "function") {
            d += performance.now(); //use high-precision timer if available
        }
        let uuid:string = ('xxxxxxxx-xxxx-' + version.toString().substr(0,1) + 'xxx-yxxx-xxxxxxxxxxxx').replace(/[xy]/g, (c) => {
            let r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d/16);
            return (c=='x' ? r : (r & 0x3 | 0x8)).toString(16);
        });

        return new UUID(uuid);
    }
}


function getProduct(id: UUID) {    
    alert(id); // alerts "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
}


const guid2 = new UUID();
console.log(guid2.toString()); // some guid string


const guid = new UUID("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx");
getProduct(guid); // ok
getProduct("notGuidbutJustString"); // errors, good

Upvotes: 5

David Sherret
David Sherret

Reputation: 106810

You could create a wrapper around a string and pass that around:

class GUID {
    private str: string;

    constructor(str?: string) {
        this.str = str || GUID.getNewGUIDString();
    }

    toString() {
        return this.str;
    }

    private static getNewGUIDString() {
        // your favourite guid generation function could go here
        // ex: http://stackoverflow.com/a/8809472/188246
        let d = new Date().getTime();
        if (window.performance && typeof window.performance.now === "function") {
            d += performance.now(); //use high-precision timer if available
        }
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
            let r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d/16);
            return (c=='x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }
}

function getProduct(id: GUID) {    
    alert(id); // alerts "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
}

const guid = new GUID("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx");
getProduct(guid); // ok
getProduct("notGuidbutJustString"); // errors, good

const guid2 = new GUID();
console.log(guid2.toString()); // some guid string

Update

Another way of doing this is to use a brand:

type Guid = string & { _guidBrand: undefined };

function makeGuid(text: string): Guid {
  // todo: add some validation and normalization here
  return text as Guid;
}

const someValue = "someString";
const myGuid = makeGuid("ef3c1860-5ce6-47af-a13d-1ed72f65b641");

expectsGuid(someValue); // error, good
expectsGuid(myGuid); // ok, good

function expectsGuid(guid: Guid) {
}

Upvotes: 32

Related Questions