1

I want to have a typescript object for color variations like this:

type Colors = {
  blue: string;
  blue[10]: string;
  blue[20]: string;
  blue[30]: string;
  blue[40]: string;
  green: string;
  green[20]: string;
  green[40]: string;
  green[60]: string;
  green[80]: string;
}

I want to be able to get a color by using just its name or a variation of the color by adding a bracket like I'm accessing an object property. Note each color has different variations. Right now I'm doing it like this:

type Variations = '' | 10 | 20 | 30 | 40;
type Variations2 = '' | 20 | 40 | 60 | 80;
type TAllColors = {
  [P in Variations as `blue${P}`]: string;
}&
{
  [P in Variations2 as `green${P}`]: string;
}

However, this creates a type thats like this:

type Colors = {
    "blue": string;
    "blue10": string;
    "blue20": string;
    "blue30": string;
    "blue40": string;
} & {
    "green": string;
    "green20": string;
    "green40": string;
    "green60": string;
    "green80": string;
}

Does anyone know how I can make it so the color variations are actually an object that I can access like blue[20] and green[80]? Thanks!

2 Answers 2

2

The big problem will be that you're trying to use blue both as the name of a string property and as the name of an object property (with properties like 10, 20, etc.) at the same time, like this:

// WON'T WORK
type Colors = {
    blue: string & {
        10: string;
        20: string;
        30: string;
        40: string;
    };
    green: string & {
        20: string;
        40: string;
        60: string;
        80: string;
    }
};

But string is a string primitive, it can't have those properties. It could be an augmented String object, like this, but note that we can't reliably use 10, 15, etc. because if the string itself is long enough, that will conflict with the usual string indexes, so we need some kind of prefix:

type Colors = {
    blue: String & {
        v10: string;
        v20: string;
        v30: string;
        v40: string;
    };
    green: String & {
        v20: string;
        v40: string;
        v60: string;
        v80: string;
    }
};
const colors: Colors = {
    blue: Object.assign(new String("blue"), {
        v10: "blue10",
        v20: "blue20",
        v30: "blue30",
        v40: "blue40",
    }),
    green: Object.assign(new String("green"), {
        v20: "green20",
        v40: "green40",
        v60: "green60",
        v80: "green80",
    }),
};

...but you'll have to remember to convert colors.blue and colors.green to primitive strings when you want primitive strings. But you can do that; the basic version might be like this:

type Variations = "v10" | "v20" | "v30" | "v40";
type Variations2 = "v20" | "v40" | "v60" | "v80";
type TAllColors = {
    blue: String & {
        [P in Variations]: string;
    };
    green: String & {
        [P in Variations2]: string;
    };
};

const colors: TAllColors = {
    blue: Object.assign(new String("blue"), {
        v10: "blue10",
        v20: "blue20",
        v30: "blue30",
        v40: "blue40",
        v50: "blue50", // We want an error here
    }),
    green: Object.assign(new String("green"), {
        v20: "green20",
        v40: "green40",
        v60: "green60",
        v80: "green80",
    }),
};

But there's a developer experience issue there, as you can see — we don't get an error for an invalid variant definition. We can fix that by using a specific function to create the objects, like this:

type Variations = "v10" | "v20" | "v30" | "v40";
type Variations2 = "v20" | "v40" | "v60" | "v80";
type TAllColors = {
    blue: String & VariationsObject;
    green: String & Variations2Object;
};

type VariationsObject = {
    [P in Variations]: string;
};
type Variations2Object = {
    [P in Variations2]: string;
};
function makeVarObject<Var>(mainValue: string, variations: Var) {
    return Object.assign(new String(mainValue), variations);
}

const colors: TAllColors = {
    blue: makeVarObject<VariationsObject>("blue", {
        v10: "blue10",
        v20: "blue20",
        v30: "blue30",
        v40: "blue40",
        v50: "blue50", // Gets the error as desired
    }),
    green: makeVarObject<Variations2Object>("green", {
        v20: "green20",
        v40: "green40",
        v60: "green60",
        v80: "green80",
    }),
};

But, I'd strongly recommend using a default property or something rather than trying to make blue and green both strings and objects so your type would look like this (it also lets us get rid of the prefix on the numbers):

type Colors = {
    blue: {
        default: string;
        10: string;
        20: string;
        30: string;
        40: string;
    };
    green: {
        default: string;
        20: string;
        40: string;
        60: string;
        80: string;
    }
};

We can do that with a quite minor change to your original code, like this:

type Variations = "default" | 10 | 20 | 30 | 40;
type Variations2 = "default" | 20 | 40 | 60 | 80;
type TAllColors = {
    blue: {
        [P in Variations]: string;
    },
    green: {
        [P in Variations2]: string;
    },
};

const colors: TAllColors = {
    blue: {
        default: "blue",
        10: "blue10",
        20: "blue20",
        30: "blue30",
        40: "blue40",
        50: "blue50", // Error as desired
    },
    green: {
        default: "green",
        20: "green20",
        40: "green40",
        60: "green60",
        80: "green80",
    },
};
Sign up to request clarification or add additional context in comments.

12 Comments

Ah, so basically there's no way of just having "blue" and also having blue[10], blue[20], etc. Not sure if I want the developer to have to type in blue.default so I guess I'll just go with what I have now and make it blue10, blue20, etc... Oh well...
Got it. Well I'm not sure if I like how logging out colors.niceNavy gives: String: "#012754". So I think I'll just go with your default method.
@MarksCode - Yeah, I think you're best off with background-color: theme.palette.indigo.default and background-color: theme.palette.indigo[50] (the last approach above). We could add a toString method to the objects so that if you used theme.palette.indigo and it was implicitly converted to string, you'd get the default.
Will do. Thanks TJ, I hope to one day be a Typescript wizard like you!
@MarksCode - LOL! I'm very much a beginner-to-medium level TypeScript person. :-)
|
0

I think what you're trying to do, even if you could type it in TypeScript (which you can't for good reasons that follow), is not implementable. The reason is that a variable or property cannot be both a primitive type and an object at the same time.

So, in your initial Colors example type you specify blue to be both a string (primitive value) and an object or array with certain indices. JavaScript is a very liberal language but it will not accept that.

Take for example:

const blue = 'abc';
blue[10] = 'def';

If you log blue you will get abc, but if you log blue[10] you will get undefined. It will look for the character at index 10 of a string.

Therefore I suggest to look for a variation of the problem, e.g. either using only strings or only objects/arrays. An alternative solution would be to use helper functions to supply the colors.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.