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",
},
};