Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow entity types to infer what traits are known to be on them #36

Open
krispya opened this issue Jan 2, 2025 · 3 comments
Open

Allow entity types to infer what traits are known to be on them #36

krispya opened this issue Jan 2, 2025 · 3 comments

Comments

@krispya
Copy link
Member

krispya commented Jan 2, 2025

The traits that are on an entity cannot be known statically, as they are dynamic by nature, but in some cases it is reasonable to infer this. For example, when an entity is queried with a Position trait and then that same entity has a get call for the position snapshot. We should be able to infer this is an entity that has a Position trait and not allow it to be possibly undefined.

I have not found a type strategy that works for this yet but I am keen to explore these possibilities.

The test case:

const Position = trait({ x: 0, y: 0 })

// The generic defines what traits are known to be on the entity
type EntityWithPosition = Entity<typeof Position>

// Only entities with Position trait can be passed in
function getPosition(entity: EntityWithPosition) {
    return entity.get(Position)
}

// However I couldn't get it to work
const position = getPosition(world.spawn(Velocity)) // Type error
@iwoplaza
Copy link
Contributor

iwoplaza commented Jan 2, 2025

I played around with this idea a bit, and managed to get a working prototype of this behavior:
https://stackblitz.com/edit/vitejs-vite-x1cqu6ts?file=src%2Fmain.ts

The problem with the solution in the linked StackBlitz is that the API for creating traits is a bit more complicated, and involves creating a unique symbol per trait explicitly:

const PositionTrait = (() => {
  const Id = Symbol();
  return trait(Id, { x: 0, y: 0 });
})();

const VelocityTrait = (() => {
  const Id = Symbol();
  return trait(Id, { x: 0, y: 0 });
})();

As soon as the code inside the IIF is extracted into a common function, the returned unique symbols are no longer differentiated from each other, as seen in the "Approach 2" section in StackBlitz.

function createTrait() {
  const Id = Symbol();
  return trait(Id, { x: 0, y: 0 });
}

const PositionTrait = createTrait();
const VelocityTrait = createTrait();

@krispya
Copy link
Member Author

krispya commented Jan 2, 2025

I was inspired by your StackBlitz so here is my attempt. First insight I got from your experiment is that in order for the entities to get a separate types for their trait generics, the generic has to be assigned to a field. That gets me further than I was before with a minimal reproduction here.

But then going to make two identical schemas look unique required a manual key entry, as you said, no matter how I tried to break TS. And this makes sense, it is by design that uniqueness only comes from static code. Here is the minimal reproduction of that.

So we have both confirmed it would require adding a key param to trait. I don't know if this is worth it just for types, but if there is another code-related reason to add it then it would be worth considering IMO.

Also is the question of if we should. Your StackBlitz example made me think twice. A natural thing to do is spawn an entity with a few start traits which would then be embedded into its type. This could be passed by reference around maintaining its type and the assumption it has those starting traits. But then the same entity could be queried any place at any time and have those traits removed making the inferred traits now a liability depending the code.

Since the traits on an entity are not actually static it is looking like this could end up exposing some nasty edge cases where the types are no longer reliable. Currently it always errs on the side of caution -- the types cannot know so you must tell TS if you know better with non-null assertion. It is annoying, but in the long run less prone to error.

@iwoplaza
Copy link
Contributor

iwoplaza commented Jan 2, 2025

But then the same entity could be queried any place at any time and have those traits removed making the inferred traits now a liability depending the code.

That is true. Unless entities become immutable, this will cause more issues that it's worth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants