styfle
styfle

Reputation: 24700

How can I define a type for a css color in TypeScript?

I have the following example code snippet:

type Color = string;

interface Props {
    color: Color;
    text: string;
}

function Badge(props: Props) {
    return `<div style="color:${props.color}">${props.text}</div>`;
}

var badge = Badge({
    color: '#F00',
    text: 'Danger'
});

console.log(badge);

Playground

I'm trying to get a build error if the color is invalid, like so:

var badge = Badge({
    color: 'rgba(100, 100, 100)',
    text: 'Danger'
});

Is there a way to define Color so that it allows only strings matching one of the following patterns?

I realize that there are colors like red and white but that might make this harder to answer if Color can accept those.

Upvotes: 58

Views: 80990

Answers (11)

tao
tao

Reputation: 90277

You can use CSS module's internal API: CSS.supports('color', yourColor)

In more detail, write a validation function and define what you want to happen when the color fails validation:

const isValidCssColor = (s: string) => {
  if (CSS.supports('color', s)) return true

  const message = `${s} is not a valid CSS color`
  throw new Error(message)

  /* or, if you don't want to throw an error:
    console.warn(message)
    return false
  // */
}

// tests
;[
  '#f00',
  '#F00F',
  '#FF0000',
  '#FF0000FF',
  'rgb(255,0,0)',
  'rgb(255 0 0)',
  'rgba(255,0,0,1)',
  'rgba(255, 0, 0, 1)',
  'hsl(0 0 0)',
  'hsl(0deg 100% 50%)',
  'hwb(0 0 0)',
  'hwb(0deg 0% 0%)',
  'lch(54 106.85 40.86)',
  'oklch(0.63 0.26 29.23)',
  'lab(54 80.82 69.9)',
  'oklab(0.63 0.22 0.13)',
  'red',
  '#i-am-not-a-css-color'
].forEach(isValidCssColor)

Usage:

if (isValidCssColor(myColor)) {
  // this code only executes if `myColor` is a valid CSS color
}

See it working in TS playground.

Upvotes: 2

Garrethta
Garrethta

Reputation: 1

You can parse that the string contains only valid chars, like

type hexSymbol =
  | "0"
  | "1"
  | "2"
  | "3"
  | "4"
  | "5"
  | "6"
  | "7"
  | "8"
  | "9"
  | "a"
  | "b"
  | "c"
  | "d"
  | "e"
  | "f";

type SkipHashSymbol<T> = T extends `#${infer Rest}` ? Rest : T;

Then go recursively through your string

type isHexSymbolsString<T> = T extends `${hexSymbol}${infer Rest}`
  ? Rest extends ""
    ? true
    : isHexSymbolsString<Rest>
  : false;

type isHex<T extends string> = isHexSymbolsString<
  SkipHashSymbol<T>
> extends true
  ? true
  : false;

type valid = isHex<"#123abc">;
type invalid = isHex<"#qwe">;

Upvotes: 0

DungGramer
DungGramer

Reputation: 2352

Install color-types can help you.

npm i -D color-types

Then add library to file tsconfig.json

{
  "compilerOptions": {
    "types": ["color-types"]
  }
}

Usages:

const color: ColorName = 'red';
const color: HEX       = '#ff0000';
const color: RGB       = 'rgb(255, 0, 0)';
const color: RGBA      = 'rgb(255 0 0 / 100%)' | 'rgba(255, 0, 0, 1)';
const color: HSL       = 'hsl(0deg 100% 50%)';
const color: HSLA      = 'hsla(0rad, 100%, 50%, 1)';
const color: HWB       = 'hwb(0turn 0% 0%)';

const color: RGBObject  = { r: 255, g: 0, b: 0 } || { red: 255, green: 0, blue: 0 };
const color: RGBAObject = { r: 255, g: 0, b: 0, a: 1 } || { red: 255, green: 0, blue: 0, alpha: 1 };
const color: HSLObject  = { h: 0, s: 100, l: 50 } || { hue: 0, saturation: 100, lightness: 50 };
const color: HSLAObject = { h: 0, s: 100, l: 50, a: 1 } || { hue: 0, saturation: 100, lightness: 50, alpha: 1 };
const color: HWBObject  = { h: 0, w: 0, b: 0 } || { hue: 0, whiteness: 0, blackness: 0 } || { hue: 0, whiteness: 0, blackness: 0, aplha: 1 };

// All of type color
const color: Color = 'red' || '#ff0000' || 'rgb(255, 0, 0)';

Upvotes: 1

Ivan Sanz Carasa
Ivan Sanz Carasa

Reputation: 1387

My 2 cents:

type RGB = `rgb(${string})`
type RGBA = `rgba(${string})`
type HEX = `#${string}`
type HSL = `hsl(${string})`
type HSLA = `hsla(${string})`
type VAR = `var(${string})`

type CssGlobals = 'inherit' | 'initial' | 'revert' | 'unset'

export type CssColor =
  | 'currentColor'
  | 'transparent'
  | RGB
  | RGBA
  | HEX
  | HSL
  | HSLA
  | VAR
  | CssGlobals

Upvotes: 8

Joseph Beuys&#39; Mum
Joseph Beuys&#39; Mum

Reputation: 2284

It's not an answer (see above for that) but here's something you might find useful:

type CssColorNames =
  | 'AliceBlue'
  | 'AntiqueWhite'
  | 'Aqua'
  | 'Aquamarine'
  | 'Azure'
  | 'Beige'
  | 'Bisque'
  | 'Black'
  | 'BlanchedAlmond'
  | 'Blue'
  | 'BlueViolet'
  | 'Brown'
  | 'BurlyWood'
  | 'CadetBlue'
  | 'Chartreuse'
  | 'Chocolate'
  | 'Coral'
  | 'CornflowerBlue'
  | 'Cornsilk'
  | 'Crimson'
  | 'Cyan'
  | 'DarkBlue'
  | 'DarkCyan'
  | 'DarkGoldenRod'
  | 'DarkGray'
  | 'DarkGrey'
  | 'DarkGreen'
  | 'DarkKhaki'
  | 'DarkMagenta'
  | 'DarkOliveGreen'
  | 'DarkOrange'
  | 'DarkOrchid'
  | 'DarkRed'
  | 'DarkSalmon'
  | 'DarkSeaGreen'
  | 'DarkSlateBlue'
  | 'DarkSlateGray'
  | 'DarkSlateGrey'
  | 'DarkTurquoise'
  | 'DarkViolet'
  | 'DeepPink'
  | 'DeepSkyBlue'
  | 'DimGray'
  | 'DimGrey'
  | 'DodgerBlue'
  | 'FireBrick'
  | 'FloralWhite'
  | 'ForestGreen'
  | 'Fuchsia'
  | 'Gainsboro'
  | 'GhostWhite'
  | 'Gold'
  | 'GoldenRod'
  | 'Gray'
  | 'Grey'
  | 'Green'
  | 'GreenYellow'
  | 'HoneyDew'
  | 'HotPink'
  | 'IndianRed'
  | 'Indigo'
  | 'Ivory'
  | 'Khaki'
  | 'Lavender'
  | 'LavenderBlush'
  | 'LawnGreen'
  | 'LemonChiffon'
  | 'LightBlue'
  | 'LightCoral'
  | 'LightCyan'
  | 'LightGoldenRodYellow'
  | 'LightGray'
  | 'LightGrey'
  | 'LightGreen'
  | 'LightPink'
  | 'LightSalmon'
  | 'LightSeaGreen'
  | 'LightSkyBlue'
  | 'LightSlateGray'
  | 'LightSlateGrey'
  | 'LightSteelBlue'
  | 'LightYellow'
  | 'Lime'
  | 'LimeGreen'
  | 'Linen'
  | 'Magenta'
  | 'Maroon'
  | 'MediumAquaMarine'
  | 'MediumBlue'
  | 'MediumOrchid'
  | 'MediumPurple'
  | 'MediumSeaGreen'
  | 'MediumSlateBlue'
  | 'MediumSpringGreen'
  | 'MediumTurquoise'
  | 'MediumVioletRed'
  | 'MidnightBlue'
  | 'MintCream'
  | 'MistyRose'
  | 'Moccasin'
  | 'NavajoWhite'
  | 'Navy'
  | 'OldLace'
  | 'Olive'
  | 'OliveDrab'
  | 'Orange'
  | 'OrangeRed'
  | 'Orchid'
  | 'PaleGoldenRod'
  | 'PaleGreen'
  | 'PaleTurquoise'
  | 'PaleVioletRed'
  | 'PapayaWhip'
  | 'PeachPuff'
  | 'Peru'
  | 'Pink'
  | 'Plum'
  | 'PowderBlue'
  | 'Purple'
  | 'RebeccaPurple'
  | 'Red'
  | 'RosyBrown'
  | 'RoyalBlue'
  | 'SaddleBrown'
  | 'Salmon'
  | 'SandyBrown'
  | 'SeaGreen'
  | 'SeaShell'
  | 'Sienna'
  | 'Silver'
  | 'SkyBlue'
  | 'SlateBlue'
  | 'SlateGray'
  | 'SlateGrey'
  | 'Snow'
  | 'SpringGreen'
  | 'SteelBlue'
  | 'Tan'
  | 'Teal'
  | 'Thistle'
  | 'Tomato'
  | 'Turquoise'
  | 'Violet'
  | 'Wheat'
  | 'White'
  | 'WhiteSmoke'
  | 'Yellow'
  | 'YellowGreen';

Upvotes: 3

Art Ginzburg
Art Ginzburg

Reputation: 123

A runtime check for color validity if (process.env.NODE_ENV === 'development') would be the best suitable solution for now, since TypeScript (as of 4.8.4) cannot handle such complex types.

I tried making a HEX color type, which seems like it should work if you just read the code as a human, but actually it fails with anything more overloaded than a short format CSS HEX color (like #fff):

type HexLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f';
type HexDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type HexPart = HexLetter | HexDigit;
type HexEncodedNumber = `${HexPart}${HexPart}`;

const hexEncodedNumberTestForSuccess: HexEncodedNumber = '0f';
const hexEncodedNumberTestForError: HexEncodedNumber = 'r3'; //* Type '"r3"' is not assignable to type 'HexEncodedNumber'.
// A single HexEncodedNumber works!
// This means we can have type safety defining a number from 0 to 255 in HEX format.

/** Length: 3 */
type HexCodeShort = `${HexPart}${HexPart}${HexPart}`;
/** Length: 6 */
type HexCodeClassic = `${HexEncodedNumber}${HexEncodedNumber}${HexEncodedNumber}`; //! Expression produces a union type that is too complex to represent. ts(2590)
// HexCodeClassic evaluates to 'any' type.
/** Length: 8 */
type HexCodeWithAlpha = `${HexCodeClassic}${HexEncodedNumber}`; // So this is `${any}aa` | `${any}ab` etc.
/** Length: 3 (short) | 6 (classic) | 8 (alpha) */
type HexCodeModern = HexCodeShort | HexCodeClassic | HexCodeWithAlpha; // Just 'any'.

type HexCodeCss = `#${HexCodeModern}`;
const hexCodeCssTestForEmergedAny: HexCodeCss = '#ICanSetAnythingHereWithNoTrouble'; // `#${any}`.
// Full CSS HEX code does not work.

So, you can only ensure full type safety for 3 characters of a CSS color, not even considering uppercase letters.

As a workaround, you can construct a function that accepts r, g, b, a all of type HexEncodedNumber, and TypeScript will type-check the parameters, but the ReturnType<> of that function will be just string, or any if you try to add as const or even 'as `#${string}`' to the return statement.

The RGB situation should be around the same, as it's the same possible number of combinations.

Upvotes: 4

zqxyz
zqxyz

Reputation: 334

It looks like you're using React in your example. A simple option may be to import CSSProperties and access the color property.

import { CSSProperties } from "react";

const Entry = ({color}: {color: CSSProperties["color"]}) => {
  return (
    <div style={{color: color}}>
      This text will be colored.
    </div>
  );
};

The benefit of this is it can easily be provided any form of color: named colors like yellow, aliceblue, or powderblue, hex values, RGB, HSL, or any string that is valid CSS.

This is functionally the same as using string but with a more expressive type usage. If you need to use type guarding, go with the first and accepted answer here.

Upvotes: 9

d3rpp
d3rpp

Reputation: 183

It is theoretically possible with union types, however if you JUST do hex codes, you have well over 16,000,000 possible combinations, and typescript (4.6.3 at least) does not like that idea.

It has been on the backburner for MS for over a year now https://github.com/microsoft/TypeScript/issues/42790

Upvotes: 4

ildar.dev
ildar.dev

Reputation: 1000

type RGB = `rgb(${number}, ${number}, ${number})`;
type RGBA = `rgba(${number}, ${number}, ${number}, ${number})`;
type HEX = `#${string}`;

type Color = RGB | RGBA | HEX;

`${...}` notation is available on new (^4.1) ts versions.

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

Upvotes: 88

Aaron Beall
Aaron Beall

Reputation: 52193

You can't do this yet in a general sense, but you can use constants and string literal types if you have a well defined set of colors:

type Color = "#FFFFFF" | "#FF0000" | "#0000FF";
const WHITE: Color = "#FFFFFF";
const RED: Color = "#FF0000";
const BLUE: Color = "#0000FF";

Obviously, this won't be practical if you want to allow any color, but in reality you probably do want to have re-usable color variables anyway.

In my project I use a script to generate a similar file from my colors.css file which defines a bunch of CSS properties:

:root {
  --primary-red: #ff0000;
  --secondary-red: #993333;
  /* etc */
}

Which gets converted to:

export const primaryRed: Color = "#ff0000";
export const secondaryRed: Color = "#993333";
// etc
export type Color = "#ff0000" | "#993333" // | etc...

And I'd use it like:

import {primaryRed} from "./Colors.ts";

interface BadgeProps {
    color: Color;
    text: string;
}  

var badge = Badge({
    color: primaryRed,
    text: 'Danger'
});

Upvotes: 9

Madara&#39;s Ghost
Madara&#39;s Ghost

Reputation: 175088

There was a proposal for a type of string which matches a pattern (regex or something else), but that proposal haven't come to fruition yet.

As a result, what you ask for is unfortunately impossible as of TypeScript 2.2.

Upvotes: 15

Related Questions