Given an descriptive object called schema
, construct a new type based on it that contains all the requirements expressed on this object with the correct Typescript typing.
My solution so far:
type ConfigParameter<IdType, ValueType> = Readonly<{
name: IdType;
type: { kind: ValueType };
}>;
type Config<T extends ReadonlyArray<ConfigParameter<string, any>>> = {
[K in T[number]["name"]]: Extract<T[number], { name: K }>["type"]["kind"];
} extends infer O
? { [P in keyof O]: O[P] }
: never;
export declare function extractType<O>(fields: O): Config<O>;
Given this sample schema:
const schema = [
{ name: "firstName", type: { kind: "STRING" } },
{ name: "age", type: { kind: "NUMBER" } },
{ name: "acceptedTerms", type: { kind: "BOOLEAN", optional: true } }
] as const;
It's possible to extract the inferred type:
export const schemaExtracted = extractType(schema);
But the returned result is as follows:
// const schemaExtracted: {
// firstName: "STRING"; WRONG, should be typed as string
// age: "NUMBER"; WRONG, should be typed as number
// acceptedTerms: "BOOLEAN"; WRONG, should be typed as optional BOOLEAN
// };
Then we can use typeof
to have a static type, but the error follows:
type SchemaTyped = typeof schemaExtracted;
// type SchemaTyped = {
// firstName: "STRING";
// age: "NUMBER";
// acceptedTerms: "BOOLEAN";
// };
And at the end, when creating a demo object using the type generated we receive the TypeScript error, that is also wrong because of the wrong extracted type
const schemaDemo: SchemaTyped = {};
// const schemaDemo: {
// firstName: "STRING";
// age: "NUMBER";
// acceptedTerms: "BOOLEAN";
// }
// Type '{}' is missing the following properties from type '{ firstName: "STRING"; age: "NUMBER"; acceptedTerms: "BOOLEAN"; }': firstName, age, acceptedTerms
What's the best way of doing this or another approximated solution?
Thanks for the assistance!
CodePudding user response:
The answer is more conditional types infer
.
Notes:
- The
ConfigParameter
probably doesn't need to be generic as it's not adding any additional information - This solution does not handle optional types yet, I will update the solution if I find a way
type ConfigParameter = Readonly<{
name: string;
type: { kind: "BOOLEAN" | "NUMBER" | "STRING", optional?: boolean };
}>;
type Config<T extends ReadonlyArray<ConfigParameter>> = {
[K in T[number]["name"]]: Extract<T[number], { name: K }>["type"]['kind'] extends infer Kind
? Kind extends "STRING"
? string
: Kind extends "BOOLEAN"
? boolean
: Kind extends "NUMBER"
? number
: never
: never;
}
const schema = [
{ name: "firstName", type: { kind: "STRING" } },
{ name: "age", type: { kind: "NUMBER" } },
{ name: "acceptedTerms", type: { kind: "BOOLEAN", optional: true } },
] as const;
type Result = Config<typeof schema>;
// ^?
Update: Deriving conditional types based on both kind
and optional
fields.
This doesn't make the optional properties "optional"
, so not ideal, but at least adds the undefined
type to the union.
CodePudding user response:
I would do it a bit differently.
I also define the ConfigParameter
type.
type ConfigParameter = {
name: string,
type: {
kind: keyof KindMap,
optional?: boolean
}
}
It does not need to be generic. It will be useful later to help TypeScript understand the shape of the object so that we can easily index it.
To convert the string literals like "STRING"
to string
, we just need a lookup map.
type KindMap = {
STRING: string
NUMBER: number
BOOLEAN: boolean
}
The conditionals work too, but they are a bit verbose.
Now to the Config
type itself.
type Config<O extends readonly ConfigParameter[]> = ({
[K in O[number] as false | undefined extends K["type"]["optional"]
? K["name"]
: never
]: KindMap[K["type"]["kind"]]
} & {
[K in O[number] as true extends K["type"]["optional"]
? K["name"]
: never
]?: KindMap[K["type"]["kind"]]
}) extends infer U ? { [K in keyof U]: U[K] } : never
We create an intersection of two mapped types: One containing all the optional: true
properties and one containing all the optional: false
or optioanl: undefined
properties.
Note: The extends infer U ? ...
stuff is only needed to properly display the type when hovering over it. Otherwise, your editor will just display Config<readonly [{ ...
which is technically correct but not really helpful.
This leads to the following result:
export declare function extractType<O extends readonly ConfigParameter[]>(fields: O): Config<O>;
const schemaExtracted = extractType(schema);
// const schemaExtracted: {
// firstName: string;
// age: number;
// acceptedTerms?: boolean | undefined;
// }