Skip to content

[Entity] Expose IdSelectorNum and IdSelectorStr types for better type safety #5073

@kakao-fleek-moon

Description

@kakao-fleek-moon

Which @ngrx/* package(s) are relevant/related to the feature request?

entity

Information

Problem

When using createEntityAdapter with a custom selectId function, the adapter.selectId property is typed as IdSelector<T>, which returns string | number. This union type creates friction when reusing selectId elsewhere in the codebase, as it requires type guards or type casting at every usage site.

Current Behavior

import { createEntityAdapter, EntityState, IdSelector } from '@ngrx/entity';

interface Product {
  id: number;
  name: string;
}

const adapter = createEntityAdapter<Product>({
  selectId: (product) => product.id, // clearly returns number
});

// adapter.selectId is typed as IdSelector<Product> = (model: Product) => string | number
const productId = adapter.selectId(someProduct); 
// productId is string | number, even though we know it's always number

// This requires type guards or casting everywhere:
if (typeof productId === 'number') {
  // use productId
}
// or
const productId = adapter.selectId(someProduct) as number;

Desired Behavior

Previously, @ngrx/entity exported IdSelectorNum<T> and IdSelectorStr<T> types from @ngrx/entity/src/models, which allowed for cleaner type casting at the definition site:

import { IdSelectorNum } from '@ngrx/entity/src/models';

export const productSelectId = adapter.selectId as IdSelectorNum<Product>;
// Now productSelectId returns number, no casting needed at usage sites

Proposed Solution

One of the following approaches would improve the developer experience:

  1. Re-export IdSelectorNum<T> and IdSelectorStr<T> types from the public API (@ngrx/entity) so developers can cast once at the definition site.

  2. Add generic parameter to createEntityAdapter to specify the ID type:

   const adapter = createEntityAdapter<Product, number>({
     selectId: (product) => product.id,
   });
   // adapter.selectId now returns number
  1. Infer the ID type from the selectId function automatically:
   const adapter = createEntityAdapter<Product>({
     selectId: (product) => product.id, // TypeScript infers number
   });
   // adapter.selectId should be (model: Product) => number

Option 3 would provide the best developer experience with zero additional effort, but any of these solutions would significantly improve type safety when working with entity adapters.

Describe any alternatives/workarounds you're currently using

Currently, we define custom type aliases and cast manually:

// Workaround: Define our own types
type IdSelectorNum<T> = (model: T) => number;
type IdSelectorStr<T> = (model: T) => string;

// Cast at definition site
export const productSelectId = adapter.selectId as IdSelectorNum<Product>;

This works but has drawbacks:

  • Each project needs to define these types locally
  • It's easy to make mistakes with manual casting
  • New team members may not know about this pattern

I would be willing to submit a PR to fix this issue

  • Yes
  • No

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