csstype icon indicating copy to clipboard operation
csstype copied to clipboard

Typing for '!Important'

Open sergeyzenchenko opened this issue 3 years ago • 3 comments

Hey guys, we've been using csstype as part of vanilla-extract. We needed to add support of !important in some cases. I've been able to implement it using following TS mapped type magic.

function withImportant(css:ImportantCSS<CSSProperties>): CSSProperties {
  return css as CSSProperties;
}

type WithImportant<T extends string> = T | `${T} !important`;
type WithImportantArrays<T> = T extends string ? WithImportant<T> : T extends Array<infer R> ? R extends string ? WithImportant<R>[] : T : T;

type ImportantCSS<T> = {
  [Property in keyof T]: WithImportantArrays<T[Property]>
}

globalStyle(`h2`, withImportant({
  textAlign: 'center !important',
  flexDirection: "row",
  border: 22
}));

It even provides code completion

image

What do you think about this approach and do you see any problems with it?

sergeyzenchenko avatar May 26 '22 17:05 sergeyzenchenko

hey @frenic

sergeyzenchenko avatar May 30 '22 08:05 sergeyzenchenko

I think it looks good. However, in the future I plan to implement string templates at some extent and I know there's a technical limit with those. It means you will receive a type error if you exceed the amount of variations. With your addition it may cause a type error at your end, but not at our end. But I don't think it will be that advanced so I would say you're pretty safe anyway.

frenic avatar Jun 01 '22 08:06 frenic

I'm just chiming in with an alternate workaround, this is how I solved it. I've noticed just before posting that it's kinda similar to OPs solution, however I still feel like it's different enough to share.

// This function requires TS 5.0, see below for alternative
function allowImportant<
  const InputProperty extends unknown,
  ReturnedPropertyType = InputProperty extends `${infer PropertyWithoutImportant} !important`
    ? PropertyWithoutImportant
    : InputProperty,
>(property: InputProperty) {
  return property as unknown as ReturnedPropertyType;
}

const autoTest = allowImportant("auto !important"); // infers as "auto"
const numberTest = allowImportant(0); // infers as 0

This function is purely a type helper, so it just returns the original value while typing it differently. If you provide it a string which ends with " !important", it infers the type as whatever is before " !important" in that string. If you provide it anything else, it will infer it as the input type.

If you remove const from before InputProperty, the function becomes compatible with TS versions earlier than TS 5.0, but the typings are looser:

function allowImportant<
  InputProperty extends unknown,
  ReturnedPropertyType = InputProperty extends `${infer PropertyWithoutImportant} !important`
    ? PropertyWithoutImportant
    : InputProperty,
>(property: InputProperty) {
  return property as unknown as ReturnedPropertyType;
}

const autoTest = allowImportant("auto !important"); // infers as string
const numberTest = allowImportant(0); // infers as number

This still worked for me with Vanilla Extract but your mileage may vary.

mikeybinns avatar Sep 16 '23 16:09 mikeybinns