1

I have the bellow function

export async function batchEntitiesBy<Entity, T extends keyof Entity>(
  entityClass: EntityTarget<Entity>
  by: T,
  variables: readonly Entity[T][]
) {
    by: T,
  variables: readonly Entity[T][]
) {
  // get entities from database ungrouped and in random order
  const entities = await db.find(entityClass, { [by]: In(variables as Entity[T][]) })

  // group the entities and order the groups by variables order
  type EntityMap = { [key in Entity[T]]: Entity[]}

  const entityMap = {} as EM;
  entities.forEach((e) => {
    if (!entityMap[e[by]]) {
      entityMap[e[by]] = []
    }
    entityMap[e[by]].push(e)
  })
  return variables.map((v) => entityMap[v]);
}

I would expect Entity[T] to give me the type of the class member specified in by and therefore entityMap to be a mapping from type(by) to type(Entity)

why am I getting this error??

Type 'Entity[T]' is not assignable to type 'string | number | symbol'.
  Type 'Entity[keyof Entity]' is not assignable to type 'string | number | symbol'.
    Type 'Entity[string] | Entity[number] | Entity[symbol]' is not assignable to type 'string | number | symbol'.
      Type 'Entity[string]' is not assignable to type 'string | number | symbol'.

Edit:

If I have an example entity

class ExampleEntity {
  a: string,
  b: number
}

I would expect

  • by to be either a or b
  • If by is a I would expect Entity[T] to be string

based on the typescript documentation https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types

Here illustrating the same problem in playgroud


Edit2:

Some example entities I would like to use with this function:

class User {
  id: string
  name: string
  address: Address
  addressId: number
}

class Address {
  id: number
  street: string
  num: number
}

example usage:

const adrIds = [1,5,2,9,4]

const users = batchEntitiesBy<User, addressId>(Users, "addressId", adrIds)
8
  • Entity[T] cannot be used as a key. Did you mean to use T? Commented Oct 20, 2022 at 16:23
  • I added my assumptions above - does this not work? Commented Oct 20, 2022 at 16:32
  • TS doesn't know that Entity[T] always maps to a valid key. Commented Oct 20, 2022 at 16:32
  • But based on my code it should right? Can I override this somehow? Or is there a way to code it in a way that it makes sense for typescript? Commented Oct 20, 2022 at 16:35
  • 1
    Show us more precise what the issue is. You can start from here: tsplay.dev/WGAX2N Commented Oct 20, 2022 at 16:39

1 Answer 1

1

To sum up the developments based of the updated question and the discussion in chat, an additional difficulty was that the original code was using the values of Entity as keys in an object (EntityMap), but not every value of Entity was a valid key (namely Address). Using a Map instead of an object was one way to solve the problem.


I think the problem with your code is that you hesitate between using the generic Entity and ExampleEntity.

In the code bellow I used ExampleEntity throughout:

type ExampleEntity = {
    a: string,
    b: number
}

const batchExampleEntitiesBy = async <By extends keyof ExampleEntity>(
    by: By, variables: readonly ExampleEntity[By][]
) => {
    const entityMap = {} as { [key in ExampleEntity[By]]: ExampleEntity[] }

    const entities = [
        { a: 'hello', b: 1 },
        { a: 'world', b: 2 },
        { a: 'hello', b: 3 },
        { a: 'world', b: 4 },
    ] as ExampleEntity[];

    entities.forEach((e) => {
        if (!entityMap[e[by]]) {
            entityMap[e[by]] = []
        }
        entityMap[e[by]].push(e)
    })

    return variables.map((v) => entityMap[v]);
};


const foo = batchExampleEntitiesBy("a", ["hello"])

In the code bellow I parameterised ExampleEntity:

type ExampleEntity = {
    a: string,
    b: number
}

type Key = string | number | symbol;

const batchEntitiesBy = <Entity extends Record<Key, Key>>() =>
    async <By extends keyof Entity>(by: By, variables: readonly Entity[By][]) => {
        const entityMap = {} as { [key in Entity[By]]: Entity[] }

        const entities = [
            { a: 'hello', b: 1 },
            { a: 'world', b: 2 },
            { a: 'hello', b: 3 },
            { a: 'world', b: 4 },
        ] as unknown as Entity[];

        entities.forEach((e) => {
            if (!entityMap[e[by]]) {
                entityMap[e[by]] = []
            }
            entityMap[e[by]].push(e)
        })

        return variables.map((v) => entityMap[v]);
    };

const batchExampleEntitiesBy = batchEntitiesBy<ExampleEntity>()

const foo = batchExampleEntitiesBy("a", ["hello"])

I imagine entities is something that you will fetch from a server. If you use the parameterised version, you should probably inject the fetching behaviour to make sure it's aligned with the type you asserted. You should also check the type of your server response at runtime.

I would personally make this function synchronous and make it expect entities as an input.

const entities = [
    { a: 'hello', b: 1 },
    { a: 'world', b: 2 },
    { a: 'hello', b: 3 },
    { a: 'world', b: 4 },
]

type Key = string | number | symbol;

const batchEntitiesBy = <
    Entity extends Record<Key, Key>,
    By extends keyof Entity
>(
    entities: Entity[],
    by: By,
    variables: readonly Entity[By][]
) => {
        const entityMap = {} as { [key in Entity[By]]: Entity[] }

        entities.forEach((e) => {
            if (!entityMap[e[by]]) {
                entityMap[e[by]] = []
            }
            entityMap[e[by]].push(e)
        })

        return variables.map((v) => entityMap[v]);
    };


const foo = batchEntitiesBy(entities, "a", ["hello"])
Sign up to request clarification or add additional context in comments.

9 Comments

Unfortunately this approach does not work for me as it makes the assumption that Entity would extends Record<key, key>. The entities I will use will be objects I fetch from a database. Is there no way to define this function such that I could pass any class in? Or at least any class out of a set of pre defined classes?
In my third attempt, the only thing batchEntitiesBy cares about is that from a list of records of a certain shape, one can query a list of lists of records of the same shape, by some filtering criteria. It's a type safe generic function. If you don't expect a generic input, you should not use a generic. Generic parameters must be statically resolved when you pass an input to the function, but fetching entities from within involves asserting that it is of a known type, and throwing at runtime if it doesn't match the schema. It's a different process.
You can totally pass a class to that function, but that's a design discussion, not a typescript discussion. You should maybe update your question to reflect the design you are after in plain Javascript so that we can help you type it, but if you had gone through the trouble of implementing the fetch in your original code, I think you would have had a better insight to start with, because that tension between having a generic and fetching data - who is responsible for the type of the data - is very tangible.
I have now expanded my function in the original question a bit to explain better what my intentions are. When I try to implement the function the way you suggested it wont let me pass in <User> as the Entity...
to be clear the point where it doesnt work is whey I do batchEntities<User, "addressId> and if I dont put that it works but gives Promise<Record<key,key>> as a return type and I need the specific one... The error I get is Type 'Address' does not satisfy the constraint 'Record<k, k>'. Index signature for type 'string' is missing in type 'Address'.ts(2344)
|

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.