I'm trying to create a generic type that would map the keys using template literals. In general i just want to created a nested type from the list of commaseparated keys:
type TFoobar = "address" | "year" | "owner.age" | "owner.address.street";
const foobar: DotToType<TFooBar> = {
address: true,
owner: {
age: true,
address: {
street: true
}
}
}
I have tried implementing it like so:
type DotToType<keys extends string> = {
[Key in keys]: Key extends `${infer Parent}.${infer Leaf}`
? {[key in Parent]: DotToType<Leaf>}
: {[k in Key]: boolean}
}[keys];
And while it does have a proper typing, it makes the properties optional (at least one is required, but there is nothing that enforces they are all present)
const foobar: DotToType<TFooBar> = {
address: true,
}; //valid, shouldnt be
const foobar: DotToType<TFooBar> = {
owner: {
age: true
},
}; //valid, shouldnt be
const foobar: DotToType<TFooBar> = {
address: true,
year: false,
owner: {
age: true,
address: {
street: true
}
}
}; //valid
I also tried doing it like this:
type DotToType<keys extends string> = {
[Key in keys as Key extends `${infer Parent}.${infer Leaf}` ? Parent : Key]:
Key extends `${infer Parent}.${infer Leaf}` ? DotToType<Leaf> : boolean
};
And while it does enforce fields, it stops working if I have multiple paths for the same object.
const foobar: DotToType<"address" | "year" | "owner.address.street"> = {
"address": true,
"year": true,
owner: {
address: {
street: true
}
}
}; // this works fine
const foobar: DotToType<"address" | "year" | "owner.address.street" | "owner.age"> = {
"address": true,
"year": true,
owner: {
address: {
street: true
}
}
}; // this only allows the first `owner.{Leaf}`
CodePudding user response:
Your initial code produces a union, which is why the properties are optional:
type DotToType<keys extends string> = {
[Key in keys]: Key extends `${infer Parent}.${infer Leaf}`
? {[key in Parent]: DotToType<Leaf>}
: {[k in Key]: boolean}
}[keys];
type MyType = DotToType<TFoobar>
// produces:
type MyType = {
address: boolean;
} | {
year: boolean;
} | {
owner: {
age: boolean;
};
} | {
owner: {
address: {
street: boolean;
};
};
}
You can use the following:
type TFoobar = "address" | "year" | "owner.age" | "owner.address.street";
type BeforeDot<K extends string> = K extends `${infer Parent}.${infer _}` ? Parent : K;
type ChildrenAfterDot<K extends string, P extends string> = K extends `${P}.${infer $Leaf}` ? $Leaf : never;
type DotToType<keys extends string> = {
[Key in BeforeDot<keys>]: Key extends keys
? boolean
: DotToType<ChildrenAfterDot<keys, Key>>
};
type MyType = DotToType<TFoobar>
// produces:
type MyType = {
address: boolean;
year: boolean;
owner: DotToType<"age" | "address.street">;
}