How To Type A Color Prop?
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'
Post a Comment for "How To Type A Color Prop?"