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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions