Issue
My objective is to take a typescript object and convert each property into a union of separate objects.
For example:
{
200: number;
500: string;
}
into
{
200: number;
500?: undefined;
} |
{
500: string;
200?: undefined;
}
I believe I was able to get this level except anytime I add a random property along with a valid property, it doesn't error out. How do I make it error out? ie
{
100: "why no error", // this should error error...
200: 100,
}
Full code sample:
type AllKeys<T> = T extends unknown ? keyof T : never;
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type _ExclusifyUnion<T, K extends PropertyKey> = T extends unknown
? Id<T & Partial<Record<Exclude<K, keyof T>, never>>>
: never;
type ExclusifyUnion<T> = _ExclusifyUnion<T, AllKeys<T>>;
type split_return_type<T> = {
[K in keyof T]: { [P in K]: T[P] };
};
type ValueOf<T> = T[keyof T];
export type SchemezLayoutReturnType<T> = Promise<
ExclusifyUnion<ValueOf<split_return_type<Required<T>>>>
>;
function TestFnx<T>(input: T, handleAsync: () => SchemezLayoutReturnType<T>) {
return {
input,
handleAsync,
}
}
type required_return_type = {
200: number;
500: string;
}
const rrr: required_return_type = {
200: 100,
500: "00"
};
TestFnx(rrr, async () => {
return {
100: "200",
200: 100,
// 500: "oii",
}
});
Reference: Playground
Solution
Disclaimer: Object types in TypeScript are not sealed, and for that reason excess or unexpected properties cannot be truly prohibited. The best you can do is discourage them. See Is it possible to constrain the types of generics to only allow known properties? for examples and a discussion.
First we can simplify your type to turn an object type like {a: string, b: number}
into a union of one-property types like {a: string, b?: never} | {a?: never, b: number}
:
type MakeUnion<T> = {
[K in keyof T]: Pick<T, K> & { [P in Exclude<keyof T, K>]?: never }
}[keyof T]
That's called a "distributive object type" (as coined in ms/TS#47109) where you make a mapped type and immediately index into it to get a union of all the properties. {[K in keyof T]: F<K>}[keyof T]
is equivalent to F<K1> | F<K2> | ⋯
for all the keys K1
and K2
in keyof T
.
The F<K>
there is Pick<T, K> & {[P in Exclude<keyof T, K>]?: never}
. Meaning for each property key in K
, we are producing the type Pick<T, K>
(using the Pick
utility type to grab the one property we do want), and intersecting it with {[P in Exclude<keyof T, K>]?: never}
(using the Exclude
utility type), prohibiting all the keys of T
which are not K
. Well, technically it makes them optional properties of the impossible never
type, but that's pretty close to prohibiting it.
Let's run a test:
type RequiredReturnType = {
200: number;
500: string;
}
type UnionOfOneProps = MakeUnion<RequiredReturnType>;
/* type UnionOfOneProps =
(Pick<RequiredReturnType, 200> & { 500?: undefined; }) |
(Pick<RequiredReturnType, 500> & { 200?: undefined; }) */
That might not be displayed exactly like {200: number, 500?: never} | {200?: never, 500: string}
, but it's equivalent.
Next you want to prevent or discourage all excess properties when calling testFnx()
. We can do it this way:
function testFnx<T, U extends MakeUnion<T> &
{ [P in Exclude<keyof U, keyof T>]: never }>(
input: T, handleAsync: () => Promise<U>) {
return {
input,
handleAsync,
}
}
I've added a second generic type parameter U
. The compiler will still infer T
from input
, and now it will infer U
from handleAsync
's return type. Then since U
is constrained to MakeUnion<T> & { [P in Exclude<keyof U, keyof T>]: never }
, it will require that handleSaync
return something that satisfies both MakeUnion<T>
(which is the part we already know about), and { [P in Exclude<keyof U, keyof T>]: never }
. This is similar to the key-prohibiting type above. Here we say that whatever keys are in U
and that are not in T
should be prohibited. Again, you can't say "prohibited", but you can say "of the impossible never
type". Unless the excess properties have type never
, which shouldn't happen, you'll get the error you expect.
Let's test that too:
const rrr: RequiredReturnType = {
200: 0,
500: "x"
};
testFnx(rrr, async () => { return { 200: 1 } }); // okay
testFnx(rrr, async () => { return { 500: "z" } }); // okay
testFnx(rrr, async () => { return { 200: 2, 500: "y" } }); // error
testFnx(rrr, async () => { return { 700: 3, 500: "x" } }); // error
// Types of property '700' are incompatible.
Looks good. testFnx()
still allows the single-prop types you wanted, and prohibits mixed-prop properties, but also prohibits objects with extra properties.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.