Reputation: 12949
We a TypeScript app and are thinking about adding a Guid
type, like this:
declare type Guid = string & { isGuid: true }
This has benefit that you cannot accidentally assign just any string to Guid
, at least without casting it first. But it has the following problem:
const a: Record<Guid, number> = { '3fa85f64-5717-4562-b3fc-2c963f66afa6': 42 };
const b: Guid = '3fa85f64-5717-4562-b3fc-2c963f66afa6';
const c: number = a[b];
// Error: Element implicitly has an 'any' type because expression of type
// 'string & { isGuid: true; }' can't be used to index type 'Record<string & { isGuid: true; }, number>'.
Is there a TypeScript setting to allow this? If I don't declare c
's type and hover over it, it correctly dispalys number
. Maybe the editor (VS Code) is using a different compiler that the TypeScript executable? Is there a way to fix it?
Upvotes: 1
Views: 210
Reputation: 7359
The below code uses TypeScript 4.1.
It's getting late here but feel free to ask me questions and I'll edit the answer later. Just as a suggestion you may want to do the below instead using type guards.
The main type is here:
type GUID<
s1 extends string = string,
s2 extends string = string,
s3 extends string = string,
s4 extends string = string,
s5 extends string = string
> = `${s1}-${s2}-${s3}-${s4}-${s5}`
It covers most cases but the cases it doesn't cover need a type guard which I split into three seperate functions:
function guidOctetPart<GP extends string, L extends number>(guidPart: GP, length: L): guidPart is string {
return guidPart.length === length;
}
function isGuid(guid: string): guid is GUID {
const [o1,o2,o3,o4,o5] = guid.split("-");
return ([[o1, 8], [o2, 4], [o3, 4], [o4, 4], [o5, 12]] as const).every(([guidPart, length]) => guidOctetPart(guidPart, length))
}
function isGuidRecord (guid: Record<GUID, number>): guid is Record<GUID, number> {
return Object.keys(guid).every(isGuid) && Object.values(guid).every(x => typeof x === "number");
}
More details in the playground link and code below... Mainly through the use of comments.
Playground Link (or look at code right below)
// This could work if this issue gets fixed https://github.com/microsoft/TypeScript/issues/34692
// type GUID<
// l1 extends number = number,
// l2 extends number = number,
// l3 extends number = number,
// l4 extends number = number,
// l5 extends number = number,
// s1 extends string = string,
// s2 extends string = string,
// s3 extends string = string,
// s4 extends string = string,
// s5 extends string = string
// > = `${s1 & {length: l1}}-${s2 & {length: l2}}-${s3 & {length: l3}}-${s4 & {length: l4}}-${s5 & {length: l5}}`
// For now we could do this... not as good but close enough.
type GUID<
s1 extends string = string,
s2 extends string = string,
s3 extends string = string,
s4 extends string = string,
s5 extends string = string
> = `${s1}-${s2}-${s3}-${s4}-${s5}`
// Guid has too few parts
const Fails1: GUID = "5151515-1161616-171717-1717171"
// Guid has too many parts
const Fails2: GUID = "5151515-1161616-171717-1717171-12341414-414141414"
// proper GUID is allowed
const Passes1: GUID = "12345678-1234-1234-1234-123456789012";
// caveat: Allows wrong lengthed-parts GUIDs
const Passes2: GUID = "1234-5151515-1161616-171717-1717171"
// TO AVOID the above caveat you should use function type guards
// Show an obviously Failing record
if(isGuidRecord({["1234"]: 1})) {
console.log("Won't ever log.") // type is always going to be a GUID if it enters here
}
// Proper GUID will log
if(isGuidRecord({[Passes1]: 2})) {
console.log("Will always log.")
}
// improper guid will not log even though it passes type checks
if(isGuidRecord({[Passes2]: 2})) {
console.log("Won't ever log.")
}
function guidOctetPart<GP extends string, L extends number>(guidPart: GP, length: L): guidPart is string {
return guidPart.length === length;
}
function isGuid(guid: string): guid is GUID {
const [o1,o2,o3,o4,o5] = guid.split("-");
return ([[o1, 8], [o2, 4], [o3, 4], [o4, 4], [o5, 12]] as const).every(([guidPart, length]) => guidOctetPart(guidPart, length))
}
function isGuidRecord (guid: Record<GUID, number>): guid is Record<GUID, number> {
return Object.keys(guid).every(isGuid) && Object.values(guid).every(x => typeof x === "number");
}
See the JS Output of the above playground:
"use strict";
// Guid has too few parts
const Fails1 = "5151515-1161616-171717-1717171";
// Guid has too many parts
const Fails2 = "5151515-1161616-171717-1717171-12341414-414141414";
// proper GUID is allowed
const Passes1 = "12345678-1234-1234-1234-123456789012";
// caveat: Allows wrong lengthed-parts GUIDs
const Passes2 = "1234-5151515-1161616-171717-1717171";
// Show an obviously Failing record
if (isGuidRecord({ ["1234"]: 1 })) {
console.log("Won't ever log."); // type is always going to be a GUID if it enters here
}
// Proper GUID will log
if (isGuidRecord({ [Passes1]: 2 })) {
console.log("Will always log.");
}
// improper guid will not log
if (isGuidRecord({ [Passes2]: 2 })) {
console.log("Won't ever log.");
}
function guidOctetPart(guidPart, length) {
return guidPart.length === length;
}
function isGuid(guid) {
const [o1, o2, o3, o4, o5] = guid.split("-");
return [[o1, 8], [o2, 4], [o3, 4], [o4, 4], [o5, 12]].every(([guidPart, length]) => guidOctetPart(guidPart, length));
}
function isGuidRecord(guid) {
return Object.keys(guid).every(isGuid) && Object.values(guid).every(x => typeof x === "number");
}
Upvotes: 1