38

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.

Rajab Shakirov
  • 7,265
  • 7
  • 28
  • 42
  • 5
    The relevant issue is at [Suggestion: Regex-validated string type](https://github.com/Microsoft/TypeScript/issues/6579). – Zev Spitz Apr 02 '17 at 09:41
  • Possible duplicate of [A typescript Guid class?](https://stackoverflow.com/questions/26501688/a-typescript-guid-class) – BuZZ-dEE Mar 29 '18 at 14:49
  • 1
    You could use a type alias, but it wouldn't provide any compile time checks. Just a hint to the developer. `type Guid = string;` – Fred Jun 27 '18 at 13:45

4 Answers4

27

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) {
}
David Sherret
  • 101,669
  • 28
  • 188
  • 178
  • 4
    but this wrapper moves my problem in other place, because I still can do: const guid = new GUID("notGuidbutJustString"). Sure I can add some runtime checking in GUID class but I want see error before application starts... – Rajab Shakirov May 10 '16 at 17:13
  • 5
    @Rajab there's no way to ensure a string value based on regex or some other function at compile time. I would recommend written unit tests to catch this problem. – David Sherret May 10 '16 at 17:19
  • It's a pity. Maybe this will change in the future but anyway your answer is helpful. Thanks! – Rajab Shakirov May 10 '16 at 17:26
  • 1
    I have created a little Typescript [npm package](https://www.npmjs.com/package/uuid-generator-ts) on [GitHub](https://github.com/BuZZ-dEE/uuid-generator-ts/tree/master/src) including unit tests. – BuZZ-dEE Mar 29 '18 at 14:36
4

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
Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
1

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

Johannes Pille
  • 4,073
  • 4
  • 26
  • 27
0

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)
}

  • Short and sweet for just validation. But once you are adding a type... might just as well give it constructor and other stuff (like in the older/more voted answers...), it might still come in handy down the road if you need to generate a uuid rather than just validating one. – Daniele Muscetta May 13 '23 at 09:27