1

So we are developing a CMS for Firestore and have created a schema system in Typescript. Users can define schemas and the data is fetch from the database with the specified schema properties.

The basic types we use:

export interface EntitySchema {
    properties: Record<string, any>;
}

interface Entity<S extends EntitySchema> {
    values: {
        [K in keyof S["properties"]]: any
    }
}

So we have the EntitySchema the developer defines, and the Entity which are the objects he gets back when fetching from the database. I include a sample method that populates the values defined in the schema:

function getEntity<S extends EntitySchema>(
    schema: S
): Entity<S> {
    return {
        values: Object.keys(schema.properties)
            .map((key) => ({ [key]: undefined }))
            .reduce((a: any, b: any) => ({ ...a, ...b }), {})
    };
}

The problem I am facing is that Typescript is not inferring properly the keys from the schema in the entity:

const sampleSchema: EntitySchema = {
    properties: {
        name: "Name"
    }
};

const entityA: Entity<typeof sampleSchema> = getEntity(sampleSchema);
const shouldFail = entityA.values.notExistingProperty; // doesn't fail

// also creating the entity directly doesn't work as expected
const entityB: Entity<typeof sampleSchema> = {
    values: {
        name: "aaa", // this is ok
        shouldFailToo: "bbb" // doesn't fail
    }
};

Either if I use the simulated DB method or initialise the Entity with a supplied schema directly, the values field in the Entity is just treated as a Record<string, any> and the keys of the schema are ignored. I feel I am missing something here. Any help appreciated!

2 Answers 2

1

Using Record<string,any> erases all the type information that typescript would otherwise infer, if you would use template parameter there.

You must somehow name type that contain properties of your document/schema. I would propose something like this:

export interface EntitySchema<T> {
    properties: T
}

Now, you build all the code around T which is your "schema properties definition":

interface Entity<T> {
    values: {
        [K in keyof T]: any
    }
}
function getEntity<T>(
    schema: EntitySchema<T>
): Entity<T> {
    return {
        values: Object.keys(schema.properties)
            .map((key) => ({ [key]: undefined }))
            .reduce((a: any, b: any) => ({ ...a, ...b }), {})
    };
}

Now, you can use it, by defining interface that contains names of allowed properties (popssibly with metadata):

interface SampleModel {
    name: string;
}
const sampleSchema: EntitySchema<SampleModel> = {
    properties: {
        name: "Name"
    }
};

And now TS is properly type-checking your code:

const entityA: Entity<SampleModel> = getEntity(sampleSchema);
const shouldFail = entityA.values.notExistingProperty; // FAILS :)

// also creating the entity directly doesn't work as expected
const entityB: Entity<SampleModel> = {
    values: {
        name: "aaa", // this is ok
        shouldFailToo: "bbb" // FAILS
    }
};

I've changed you model a little, here is TS Playground link

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

1 Comment

Wow thanks a lot!! This definitely points me in the right direction! The only issue I have is that I really need the schema properties to have a record shape. I have simplified the example but the generic you call <T> needs to be a Record<string, Property>, being Property another interface that looks like: ``` interface Property { type: "string" | "number" ... } ```
0

I managed to preserve type information using a record, thanks to the lead of Zbigniew Zagórski :)

export interface EntitySchema {
    properties: Record<string, any>;
}

interface Entity<S extends EntitySchema> {
    values: Record<Extract<keyof S["properties"], string>, any>
}

// simulate getting an entity from a db with a schema
function getEntity<S extends EntitySchema>(
    schema: S
): Entity<S> {
    return {
        values: Object.keys(schema.properties)
            .map((key) => ({ [key]: undefined }))
            .reduce((a: any, b: any) => ({ ...a, ...b }), {})
    };
}

const schema = {
    properties: {
        name: "Name",
        ppp: "a"
    }
};
const entity = getEntity(schema);
const ok = entity.values.name;
const shouldFail = entity.values.notExistingProperty; // fails

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.