Skip to content

Any array is a valid QueryOrderMap<T> if the entity T has a property length #2829

@gabubarks

Description

@gabubarks

Describe the bug

Summary:

Strict typing for passing an array to orderBy argument does not work if the entity has a property length. Contents of the array are not type checked.

Long description:

I ran into this issue while updating some of my utility functions to also accept an orderBy argument typed as QueryOrderMap<T> | QueryOrderMap<T>[] just like in FindOptions:

orderBy?: QueryOrderMap<T> | QueryOrderMap<T>[];

My code seemed to work, but for some reason type checking on my orderBy argument did not work for one specific entity (but it did work for all others). Namely if I passed an array of objects as an argument to my function it would not throw any compiler errors even if the properties did not exist on that entity. Later I found out that this even occurs with regular EntityManager.find, i.e.

em.find(SomeEntity, {}, {
    orderBy: [
        { someNonExistentProperty: 'and whatever value' },
    ]
})

while if I manually specified the type as follows, then I would get a type error when compiling, as expected:

em.find(SomeEntity, {}, {
    orderBy: [
        { someNonExistentProperty: 'and whatever value' },
    ] as QueryOrderMap<SomeEntity>[]
})

For some reason the type of the array was being inferred as QueryOrderMap<SomeEntity> instead of QueryOrderMap<SomeEntity>[] and for some reason an array was accepted as that type... After being stumped for a while I found out that removing property length on my entity would cause compilation to throw the correct error.

Then it all clicked.

export type QueryOrderMap<T> = {
[K in keyof T as ExcludeFunctions<T, K>]?: QueryOrderKeys<ExpandProperty<T[K]>>;
};

Any array is a valid QueryOrderMap<T> (even if it's not a valid QueryOrderMap<T>[]) if the entity T has a property length, because the only non-function property of any array is also length.

Passing an array to orderBy for an entity with a length property will throw all type safety out the window. The contents of the array can be anything.

To Reproduce
Define an entity with the property named length:

@Entity()
export class SomeEntity {
    @PrimaryKey()
    id!: number

    @Property()
    length!: number

    @Property()
    otherProperties!: number
}

Now passing an array (with arbitrary contents) as orderBy argument passes compilation instead of giving a type error.

em.find(SomeEntity, {}, {
    orderBy: [
        { someNonExistentProperty: 'and whatever value' },
    ]
})

Expected behavior
Compilation should fail with for example the following type error:

src/test.ts:46:19 - error TS2322: Type '{ someNonExistentProperty: string; }' is not assignable to type 'QueryOrderMap<SomeEntity>'.
  Object literal may only specify known properties, and 'someNonExistentProperty' does not exist in type 'QueryOrderMap<SomeEntity>'.

46                 { someNonExistentProperty: 'and whatever value' },
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Additional context
My knowledge of Typescript types and generics is not deep enough to suggest any specific fix, and also I can see how this might just be an issue in general if you define your interface (any interface) to have a property length, that arrays will match that type as well.

Hope this rambling analysis made sense, and thanks for all the great work!

Versions

Dependency Version
node v16.13.2
typescript 4.5.5
mikro-orm 5.0.5
your-driver postgres

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions