2

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!

2 Answers 2

2

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;
// }

Playground

Sign up to request clarification or add additional context in comments.

4 Comments

Beautiful solution, you taught me how to use the as clause in mapped types
Thanks for the explanation, your solution is quite clever.
Now, please, how can I compile this without receiving the following error ReferenceError: extractType is not defined? I'm trying to create a function for this, but a little with hard time to get the return type to work.
Seems like you have not defined the function extractType. Check the last code block of my answer or the playground link to see how I defined it.
2

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

TS playground

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.

TS playground

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.