Skip to content Skip to sidebar Skip to footer

How To Type A Color Prop?

My component accepts an overlay prop that is supposed to be a valid CSS color. When I CTRL + Click on a color property in style object, the type definition comes from a csstype fol

Solution 1:

This one is pretty hard to encode in TypeScript's type system. I believe a full fledged parser can do a better job in both speed and accuracy.


Anyway, if you really want to get some typecheking for your color values from typescript then let's start with w3c color property description:

Values: <colorvalue> | <colorkeyword> | currentColor | transparent | inherit

playground link for those who don't need explanations and what to look right into the code.


Well, color keyword, currentColor, transparent and inherit are pretty straightforward:

typeColor = ColorValue | ColorKeyword | 'currentColor' | 'transparent' | 'inherit'typeColorKeyword =
    | "black"
    | "silver"
    | "gray"
    ...
    | "rebeccapurple"

The tricky part is <color value>:

The color can be specified as

* a hexadecimal RGB value: #faf or #ffaaff
* a RGB value: rgb(255, 160, 255) or rgb(100%, 62.5%, 100%)
      Each value is from 0 to 255, or from 0% to 100%.
* a RGBA value: rgba(255, 160, 255, 1) or rgba(100%, 62.5%, 100%, 1)
      This variant includes an “alpha” component to allow 
      specification of the opacity of a color. Values are 
      in the range 0.0 (fully transparent) to 1.0 (fully opaque).
* a HSL value: hsl(0, 100%, 50%)
      A triple (hue, saturation, lightness). hue is an 
      angle in degrees. saturation and lightness are 
      percentages (0-100%).
* a HSLA value: hsla(0, 100%, 50%, 1)
      This variant includes an “alpha” component to allow 
      specification of the opacity of a color. Values are 
      in the range 0.0 (fully transparent) to 1.0 (fully opaque).

hexadecimal RGB value is still ok-ish:

typeHexDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'typeHex3 = `${HexDigit}${HexDigit}${HexDigit}`typeRGBColor<T extendsstring> = 
  Lowercase<T> extends`#${Hex3}`
      ? T
      : Lowercase<T> extends`#${Hex3}${infer Rest}`
        ? RestextendsHex3
            ? T
            : never
        : never

We have to introduce type variable T. Otherwise 'flat' union type:

typeRGBColor = `#${Hex3}` | `#${Hex3}${Hex3}`

is going to consist of 16^3 + 16^6 constituents that's far beyound 100000 typescript limit for union types.

Let's introduce some helper types to work with numbers. We have to check the percents are not greater than 100% and end with % character.

typeDecDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'typeDigits0to4 = '0' | '1' | '2' | '3' | '4'typeOnlyDecDigits<T extendsstring> = 
    T extends`${DecDigit}${infer Rest}`
        ? Restextends''
            ? 1
            : OnlyDecDigits<Rest>
        : nevertypeIsDecNumber<T extendsstring> =
    T extends`${infer Integer}.${infer Fractional}`
        ? Integerextends''
            ? OnlyDecDigits<Fractional>
            : Fractionalextends''
                ? OnlyDecDigits<Integer>
                : OnlyDecDigits<Integer> & OnlyDecDigits<Fractional>
        : OnlyDecDigits<T>

typeIntegerPart<T extendsstring> =
    T extends`${infer I}.${infer F}`
        ? I
        : T

typeIsInteger<T extendsstring> = 
    1extendsIsDecNumber<T>
        ? T extendsIntegerPart<T> 
            ? 1 
            : never
        : nevertypeLess100<T extendsstring> = 
    IsDecNumber<T> extends1
        ? IntegerPart<T> extends`${DecDigit}` | `${DecDigit}${DecDigit}` | '100'
            ? 1
            : never
        : nevertypeIsPercent<T extendsstring> =
    '0'extends T
        ? 1
        : T extends`${infer P}%` 
            ? Less100<P> 
            : never

Also color values must be integers and not greater than 255:

typeColor255<T extendsstring> =
    1extendsIsInteger<T>
        ? T extends`${DecDigit}` 
                  | `${DecDigit}${DecDigit}` 
                  | `1${DecDigit}${DecDigit}` 
                  | `2${Digits0to4}${DecDigit}`
                  | `25${Digits0to4 | '5'}`
            ? 1
            : never
        : never

so, any color value can be encoded as an integer number in [0..255] range or a percent:

typeIsColorValue<T extendsstring> = IsPercent<T> | Color255<T>

Adding utility Trim type to trim extra spaces on both ends:

typeWhiteSpace = ' 'typeTrim<T> = T extends`${WhiteSpace}${infer U}` 
    ? Trim<U> 
    : T extends`${infer U}${WhiteSpace}` 
        ? Trim<U> 
        : T;

That's enough for rgb:

typeRGB<T extendsstring> = 
    T extends`rgb(${infer R},${infer G},${infer B})` 
        ? '111'extends`${IsColorValue<Trim<R>>}${IsColorValue<Trim<G>>}${IsColorValue<Trim<B>>}`
            ? T
            : never
        : never

For rgba/hsla we'll need opacity. Here we just ask for any valid number or a percent:

typeOpacity<T extendsstring> = IsDecNumber<T> | IsPercent<T>

Now we can check rgba values:

typeRGBA<T extendsstring> =
    T extends`rgba(${infer R},${infer G},${infer B},${infer O})`
        ? '1111'extends`${IsColorValue<Trim<R>>}${IsColorValue<Trim<G>>}${IsColorValue<Trim<B>>}${Opacity<Trim<O>>}`
            ? T
            : never
        : never

Adding degree checker for hsl/hsla:

typeDegree<T extendsstring> =
    1extendsIsInteger<T>
        ? T extends`${DecDigit}`
                  | `${DecDigit}${DecDigit}`
                  | `${'1' | '2'}${DecDigit}${DecDigit}`
                  | `3${Digits0to4 | '5'}${DecDigit}`
                  | '360'
            ? 1
            : never
        : never

and finally we can cover the last cases:

typeHSL<T extendsstring> =
    T extends`hsl(${infer H},${infer S},${infer L})`
        ? `111`extends`${Degree<Trim<H>>}${IsPercent<Trim<S>>}${IsPercent<Trim<L>>}`
            ? T
            : never
        :nevertypeHSLA<T extendsstring> =
    T extends`hsla(${infer H},${infer S},${infer L},${infer O})`
        ? `1111`extends`${Degree<Trim<H>>}${IsPercent<Trim<S>>}${IsPercent<Trim<L>>}${Opacity<Trim<O>>}`
            ? T
            : never
        :never

So our final type will look like that:

typeColorValue<T extendsstring> = HexColor<T> | RGB<T> | RGBA<T> | HSL<T> | HSLA<T>

typeColor<T extendsstring> = ColorValue<T> | ColorKeyword | 'currentColor' | 'transparent' | 'inherit'

playground link

Post a Comment for "How To Type A Color Prop?"