frenic / csstype

Strict TypeScript and Flow types for style based on MDN data
MIT License
1.7k stars 69 forks source link

Typing for '!Important' #160

Open sergeyzenchenko opened 2 years ago

sergeyzenchenko commented 2 years ago

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 commented 2 years ago

hey @frenic

frenic commented 2 years ago

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.

mikeybinns commented 9 months ago

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.