2

I'm attempting to do the exact same as a lot of other developers probably have been trying since TypeScript 4.1 has been released: Strictly typing of all strings with predetermined patterns.

Although I managed to find a nice compromise for date strings, I'm now facing the challenge of Hex color codes.

Obviously the naive way of attempting a HEX = 0 | 1 | ... | "E" | "F" and then declaring

type HEX_CODE = `#${HEX}${HEX}${HEX}${HEX}${HEX}${HEX}`;

makes the type too complex by the union of too many types.

Fair enough.

So I figure I'd attempt to at least add the requirement of the # in front of the hex code, meaning I'd settle for something like:

type HEX_CODE = `#${arbitraryString}`;

However I can't figure out a way to get this to work. Does anyone know how to either make this work, or perhaps another (maybe better) compromise on the type?

Lars Holdaas
  • 691
  • 1
  • 4
  • 24

2 Answers2

2

So isn't this just:

type HEX_CODE = `#${string}`;

typescript playground

TkDodo
  • 20,449
  • 3
  • 50
  • 65
  • 2
    To elaborate, the OP had the right idea with `type HEX_CODE = \`#${arbitraryString}\`;`. But since a type is being declared here, the bit inside of `${}` needs to be a type, not a variable reference. – jered Mar 29 '21 at 09:46
  • 1
    These are called "pattern literal types" according to the PR that implemented them, [microsoft/TypeScript#40598](https://github.com/microsoft/TypeScript/pull/40598). – jcalz Mar 29 '21 at 14:42
1

UPDATE 28 August 2022

See this answer and updated article

Here you have a solution:

UPDATE

Now, it throws an error if string length is not equal 6

type HexNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type HexString = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
type StringNumber<T extends number> = `${T}`
type HEX = HexNumber | StringNumber<HexNumber> | HexString;

type Check<T extends string, Cache extends readonly string[] = []> =
    T extends `${infer A}${infer Rest}`
    ? A extends HEX
    ? Check<Rest, [...Cache, A]> : A extends ''
    ? 1 : 2 : T extends '' ? Cache extends { length: 6 }
    ? Cache : 'String should have 6 chars. No more, no less' : never;

type Elem = string;

// before reading keep in mind that `: never` indicates that this conditional type has reached an invalid state
// when type checking linter/compiler will throw an error on any invalid type instantion
// e.g. let a: string = 1
type Mapper<
    Arr extends ReadonlyArray<Elem>,
    Result extends string = ''
    > = 
    Arr extends [] // is the array empty?
      // yes, the array is empty
      ? Result
      //no, the array is not empty, 
      : Arr extends [infer H] // does the array contain a single value of type H with properties as follows?
          // here is a property of H such that H is valid in this context
          ? H extends Elem   // does H exend Elem?
          // yes, Arr extends [Elem]
          ? `${Result}${H}` // the type parameters are valid and Mapper is this type
          : never           // no, Mapper has invalid type parameters
          // no, the array contains more than 1 value
          : Arr extends readonly [infer H, ...infer Tail] // does Arr extend the type of an array with a single value of type H followed by the values within an array of type Tail? e.g. ['hellp', 1, 2, 3] or ['hello', 'world']
            // here is a property of Tail such that Tail is valid in this context
            ? Tail extends ReadonlyArray<Elem> // does Tail extends a ReaonlyArray containing elements of type Elem  e.g. [Elem] ?
              // yes, Tail fits our assumption
              ? H extends Elem // does H extend Elem? (same as above)
                ? Mapper<Tail, `${Result}${H}`>  // yes! now recurively build the `Result` type!
                : never // no, H is invalid 

              : never // no, Tail is the wrong type since it contains values of a type other than Elem
              
            : never // no, Arr doesn't extend an array of this shape;


type Result = Mapper<Check<'abcdef'>> // allow
type Result2 = Mapper<Check<'00cdef'>> // allow
type Result3 = Mapper<Check<'z0cdef'>> // not allow
type Result4 = Mapper<Check<'00cdem'>> // not allow
type Result5 = Mapper<Check<'aaaaa'>> // to few arguments

Playground

U can use my solution only for function arguments in practice

const hex = <T extends string, U extends {
    'valid': 'valid',
    'invalid': 'invalid',
}[Check<T> extends string[] ? Mapper<Check<T>> extends T ? 'valid' : never : 'invalid']>(value: T, ...rest: U extends 'valid' ? [] : [never]) => value

const result = hex('aaaaaf') // ok
const result2 = hex('aaaaaZ') // error

Playground 2

  • While this certainly looks interesting, how do I use this in an actual setting? Like say I want to declare a variable with the restrictions? With this it seems I'd make constants of type Result which binds it to strictly equal 'abcdef', which fairly enough is a valid 6-length HEX but I'd like my variables to hold any possible 6-length HEX values (preferrable prefixed with #) – Lars Holdaas Mar 30 '21 at 01:09
  • U are absolutely right. I made an update. U can use my solution only for function arguments – captain-yossarian from Ukraine Mar 30 '21 at 07:27