Tighten up types for Entity API#7560
Conversation
|
Wow @hmans - we make a great team. 😄 Thanks for your help! Check out the updated PR. Seems to work great. entity-typings.mp4Here are the docs: What do you think? |
|
This is pretty close to my land. I do have custom physics components that get introduced once you import our Jolt physics library: github.com/gamebop/physics It will create multiple new components, like 'body'. I just had to suck up the errors, since, I concured I am in minority of users who introduce new components to the system. However, I am aware of the strides towards supporting custom addons/components and was holding off till closed beta is out. My library will definitely benefit from this PR. Good motion! Also worth mentioning that a factory component could add Debug asserts for supplied properties. We do it to spot any unrecognized properties, mostly due to typos. A component may throw a warning if a property is not recognized, or values are out of acceptable range. |
|
@LeXXik Wow, that physics library looks really cool! Nice job! @hmans I do have one concern about something. Before adding support for custom components, if you tried to add an invalid property to the options object, IntelliSense would show a red line under that property and log an error in the PROBLEMS panel. Now, if you do that, IntelliSense thinks it's just a custom component. Hmmm. I'm wondering if we can say a custom component is a string but NOT any of the keyof ComponentMap values. |
Yeah, this is the one caveat I was writing about in my post. I have some more ideas for stuff I want to try to improve this. I won't be able to get to it until later tonight, but maybe we can figure something out. My main issue that's slowing me down right now is that I'm having a hard time figuring out a speedy development/testing flow for this. I'm diving into this codebase for the first time, and it's also the first time I am working with jsdoc types (as opposed to plain old TS, which I'm very familiar with.) So far, the only way I've found to get my changes to Right now I'm trying the changes from within the test scripts in the engine project, |
|
Technically we should have everything that's needed from this point: createOptions.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem,
pc.AnimComponentSystem
];Then just do some type voodoo. Minimal example with a new component: class OceanComponent {}
class OceanComponentData {}
class OceanComponentSystem extends ComponentSystem {
/** @type {'ocean'} */
id = 'ocean';
ComponentType = OceanComponent;
DataType = OceanComponentData;
}
const systems = [OceanComponentSystem];
console.log("systems", systems);
/**
* @typedef {{
* [S in (typeof systems)[number] as InstanceType<S>['id']]: InstanceType<InstanceType<S>['ComponentType']>
* }} ComponentMap
*/So we get the actual types we are using: Instead of simply registering every PC system. Why would I even want types for e.g. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
src/framework/entity.js:452
removeComponentis now typed askeyof ComponentMap, but the runtime implementation supports any registered system id (including custom component systems) and the same method already has a custom-component overload foraddComponent. This change makes it impossible for TypeScript users to remove custom components (or built-in but not listed inComponentMap) without casting. Consider adding overloads similar toaddComponent(built-in:keyof ComponentMap; custom:string) or widening this param back tokeyof ComponentMap | stringwhile keeping the narrowed overload for known built-ins.
* Remove a component from the Entity.
*
* @param {keyof ComponentMap} type - The name of the Component type.
* @example
* const entity = new pc.Entity();
* entity.addComponent("light"); // add new light component
*
* entity.removeComponent("light"); // remove light component
*/
removeComponent(type) {
src/framework/entity.js:476
findComponentis now restricted tokeyof ComponentMap, but it works at runtime for any component id stored inentity.c[...](including custom component systems). Tightening this to built-ins will force casts for common patterns (e.g. looking up by a dynamic string). Consider adding a second overload forstringthat returnsComponent | undefined, while keeping the keyed overload for strong typing on built-ins.
* Search the entity and all of its descendants for the first component of specified type.
*
* @template {keyof ComponentMap} T
* @param {T} type - The name of the component type to retrieve.
* @returns {ComponentMap[T] | undefined} A component of specified type, if the entity or any
* of its descendants has one. Returns undefined otherwise.
* @example
* // Get the first found light component in the hierarchy tree that starts with this entity
* const light = entity.findComponent("light");
*/
findComponent(type) {
src/framework/entity.js:492
- Same issue as
findComponent:findComponentsis now limited tokeyof ComponentMap, but the underlying storage (entity.c) supports arbitrary system ids (including custom components). Consider providing an additional overload acceptingstringand returningComponent[]so dynamic/custom lookups remain type-safe without casts.
* Search the entity and all of its descendants for all components of specified type.
*
* @template {keyof ComponentMap} T
* @param {T} type - The name of the component type to retrieve.
* @returns {ComponentMap[T][]} All components of specified type in the entity or any of its
* descendants. Returns empty array if none found.
* @example
* // Get all light components in the hierarchy tree that starts with this entity
* const lights = entity.findComponents("light");
*/
findComponents(type) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * @template {keyof ComponentMap} T | ||
| * @param {T | string} type - The type of component to add. | ||
| * @param {Partial<ComponentMap[T]> | any} [data] - Optional initialization data for the component. |
There was a problem hiding this comment.
In the implementation signature, Partial<ComponentMap[T]> | any collapses to any, which can negate the benefit of having a typed data parameter in some tooling/emit paths. Consider using unknown (or object) instead of any here, and rely on the overloads to provide the correct data typing for built-in vs custom components.
| * @param {Partial<ComponentMap[T]> | any} [data] - Optional initialization data for the component. | |
| * @param {unknown} [data] - Optional initialization data for the component. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Collapses the two `addComponent` JSDoc overloads into a single signature using a conditional type for both `data` and the return type. This fixes the silent-fallback caveat where mistyping a property of `data` for a known built-in component would resolve to the permissive custom-component overload instead of erroring. Applies the same pattern consistently to `removeComponent`, `findComponent`, and `findComponents` so all four methods support arbitrary string IDs for custom component systems while still providing strict typing and IntelliSense for built-ins. Made-with: Cursor







At the moment, the Entity API has reasonably weak typings. This PR improves the situation, particularly for
addComponent.I confirm I have read the contributing guidelines and signed the Contributor License Agreement.