I'm looking for a type Validate
that would enforce something like this
// validate
const myObj: Validate<'value', boolean> = {
value: true,
otherValue: 'its value',
nestedProp: {
value: false
}
}
// does not validate
const myObj: Validate<'value', boolean> = {
value: true,
otherValue: 'its value',
nestedProp: {
value: 0 // not of type boolean
}
}
Where the first parameter of the generic is the name of the nested property, and the 2nd is the type to enforce.
Is this even possible?
Edit: the idea is to get auto completition from the IDE when union types are used instead of boolean
in that example.
type ColorType = 'red' | 'green'
type ColoredObject<T> = {
[P in keyof T]: T[P] extends object ? ColoredObject<T[P]> : T[P];
} & { color: ColorType };
const myObject: ColoredObject<{ prop1: { prop2: { color: string } } }> = {
color: 'red',
prop1: {
color: 'red',
prop2: {
color: 0 // this would fail
}
}
};
In the example above, it works the way I want it, but it makes the color attribute required. Is there a way to make it optional without breaking the requirement ? Also this requires me to pass the object interface as generic, which I do not want since I might not know it.
Edit2: Or this, but then it will not allow any other props than color
type ColoredObject<T, P, V> = {
[P in keyof T]: T[P] extends object ? ColoredObject<T[P], P, V> : T[P] extends 'color' ? V : never
}
type ColorType = 'red' | 'green'
const test: ColoredObject<any, 'color', ColorType> = {
color: 'green',
otherProps: 'lala', // <---- fails here
props: {
color: 'green'
}
}
CodePudding user response:
what about that ?
type Validate<K extends string, T> = {
value: boolean,
otherValue: string,
nestedProp: {
[key in K] :T
}
}
Thanks to Titian comment
type Validate<K extends string, T> = {
value: boolean,
otherValue: string,
nestedProp: Record<K, T>
}
CodePudding user response:
It isn't obvious that, given a key type K
and a value type V
, that there is a specific type Validate<K, V>
which exactly enforces the rule you're looking to enforce. Often when it's difficult or impossible to write a specific type corresponding to some rule, it becomes a lot more straightforward to write a generic type that checks if a candidate type obeys that rule.
So instead of Validate<K, V>
, you use T extends Validate<T, K, V>
as a self-referential type constraint. And to prevent you from needing to manually specify that T
type, you can write a generic helper function to infer it for you.
So conceptually the approach is: instead of const x: Validate<K, V> = ...
, you write a function of the form
declare const validateKV = <T extends Validate<T, K, V>>(t: T) => t;
and then write const x = validateKV(...);
. Of course, it's not even that easy all the time; often T extends Validate<T, K, V>
would be considered an illegally circular constraint, and to work around that you need to play games with conditional types like
declare const validateKV = <T,>(
t: T extends Validate<T, K, V> ? T : Validate<T, K, V>
) => t;
And you probably don't want to write a new helper function for every possible K
and V
, so that should be generic also, and unfortunately you can't get "partial inference" where you specify K
and V
but the compiler infers V
(see microsoft/TypeScript#26242 for the feature request) so you need to work around that (see Typescript: infer type of generic after optional first generic for more information) and the easiest way is currying, like this:
const validate = <K extends PropertyKey, V>(k: K, v: V) =>
<T,>(t: T extends Validate<T, K, V> ? T : Validate<T, K, V>) => t;
So that's what we'll use, once we define Validate<T, K, V>
.
And here it is:
type Validate<T, K extends PropertyKey, V> =
T extends object ? {
[P in keyof T]: P extends K ? V : Validate<T[P], K, V>
} : T;
The idea is that, if T
is a primitive type, then Validate<T, K, V>
is just T
. So Validate<T, K, V>
will automatically accept any primitive type. On the other hand, if it is an object type, then Validate<T, K, V>
is a mapped type where each property key P
is checked. If the property key P
happens to be the same as the special key K
, then we want to make sure that it has value type V
. Otherwise, we just want to recurse down and make sure that the property type T[P]
is assignable to Validate<T[P], K, V>
for the same K
and V
.
Let's test it out, for the particular K
of "value"
and V
of boolean
, as mentioned in the question code. First we need to use the curried outer function:
const validateValueBoolean = validate("value", Math.random() < 0.5);
And now we can try that.
const myObj = validateValueBoolean({
value: true,
otherValue: 'its value',
nestedProp: {
value: false,
nested: {
value: true
}
},
me: {
value: true,
other: 23,
somethingElse: "",
whoKnows: {
foo: {
value: true
},
value: false
}
}
}); // okay
That type checks as desired. At every level, the property named value
has a value of type boolean
. Contrast that with:
const myObj2 = validateValueBoolean({
value: true,
otherValue: 'its value',
nestedProp: {
value: false,
nested: {
value: true
}
},
me: {
value: true,
other: 23,
somethingElse: "",
whoKnows: {
foo: {
value: 3 // error, number is not a boolean
},
value: false
}
}
});
This is a compiler error, and specifically on the bad value
property, where it tells you that 3
is a number
whereas a boolean
was expected. And if you fix that then the error goes away.
So, looks good. This is about as close as I can imagine getting to your desired type, given the limitations of the language and my understanding of the use case.