Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Infer type of object of classes

Building a form config builder, want to infer types dynamically, here’s the minimal reproduction code

interface Meta {
    optional: boolean
}

class Base<Type = any> {
    readonly _type!: Type
    baseMeta: Meta

    constructor() {
        this.baseMeta = {
            optional: false,
        }
    }

    optional() {
        this.baseMeta.optional = true
        return this;
    }
}

class FieldText extends Base<string> {

}

class FieldNumber extends Base<number> {

}


class Field {
    static text() {
        return new FieldText();
    }

    static number() {
        return new FieldNumber();
    }
}

const fields = {
    fieldText: Field.text(),
    fieldNumber: Field.number(),
    fieldTextOptional: Field.text().optional(),
    fieldNumberOptional: Field.number().optional(),
}

Given code above I want to achieve one of the following types, doesn’t matter which one

type ExpectedType = {
    fieldText: string;
    fieldNumber: number;
    fieldTextOptional: number | undefined;
    fieldNumberOptional: number | undefined;
}

Also fine

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

type ExpectedTypeWithOptionalKeys = {
    fieldText: string;
    fieldNumber: number;
    fieldTextOptional?: number;
    fieldNumberOptional?: number;
}

I’ve gotten to infering types, but can’t figure out why the optional property is not taken in consideration

type ExtractFieldType<O, T> = O extends true ? T | undefined : T;

type SchemaType<T extends Record<string, Base>> = {
    [Property in keyof T]: ExtractFieldType<T[Property]['baseMeta']['optional'], T[Property]['_type']>;
};

type Schema = SchemaType<typeof fields>;

type Schema results in:

type Schema = {
    fieldText: string;
    fieldNumber: number;
    fieldTextOptional: string;
    fieldNumberOptional: number;
}

this O extends true ? T | undefined : T always returns T, doesn’t take actual value of optional into consideration

>Solution :

The type of optional in Meta is always boolean. You need to store the value in a generic to use later:

interface Meta<Optional extends boolean> {
    optional: Optional;
}

class Base<Type = any, Optional extends boolean = false> {
    readonly _type!: Type
    baseMeta: Meta<Optional>

    constructor() {
        this.baseMeta = {
            optional: false as Optional,
        }
    }

    optional(): Base<Type, true> {
        this.baseMeta.optional = true as Optional; // needs unsafe assert, or alternatively //@ts-ignore
        return this as Base<Type, true>;
    }
}

Then when you change it with optional, trick TypeScript into believing you are returning Base<Type, true> instead.

You only need to change this. Your original solution now works.


Addressing the followup in the comments:

optional<T extends boolean = true>(value: T = true): Base<Type, T> {
    this.baseMeta.optional = value as Optional;
    return this as Base<Type, T>;
}
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading