Reputation: 8035
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
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
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
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')
Upvotes: 1
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
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