Home > Software engineering >  Create a new type based on object properties
Create a new type based on object properties

Time:10-16

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

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

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

Playground

  • Related