{"id":23543,"date":"2023-11-27T00:08:37","date_gmt":"2023-11-26T23:08:37","guid":{"rendered":"https:\/\/www.angulararchitects.io\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/"},"modified":"2023-12-11T18:01:26","modified_gmt":"2023-12-11T17:01:26","slug":"ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions","status":"publish","type":"post","link":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/","title":{"rendered":"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions"},"content":{"rendered":"<div class=\"wp-post-series-box series-ngrx-signal-store-en wp-post-series-box--expandable\">\n\t\t\t<input id=\"collapsible-series-ngrx-signal-store-en69f5f554d241e\" class=\"wp-post-series-box__toggle_checkbox\" type=\"checkbox\">\n\t\n\t<label\n\t\tclass=\"wp-post-series-box__label\"\n\t\t\t\t\tfor=\"collapsible-series-ngrx-signal-store-en69f5f554d241e\"\n\t\t\ttabindex=\"0\"\n\t\t\t\t>\n\t\t<p class=\"wp-post-series-box__name wp-post-series-name\">\n\t\t\tThis is post 3 of 7 in the series <em>&ldquo;NGRX Signal Store&rdquo;<\/em>\t\t<\/p>\n\t\t\t<\/label>\n\n\t\t\t<div class=\"wp-post-series-box__posts\">\n\t\t\t<ol>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/the-new-ngrx-signal-store-for-angular-2-1-flavors\/\">The new NGRX Signal Store for Angular: 3+n Flavors<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/smarter-not-harder-simplifying-your-application-with-ngrx-signal-store-and-custom-features\/\">Smarter, Not Harder: Simplifying your Application With NGRX Signal Store and Custom Features<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><span class=\"wp-post-series-box__current\">NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions<\/span><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/the-ngrx-signal-store-and-your-architecture\/\">The NGRX Signal Store and Your Architecture<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/using-the-resource-api-with-the-ngrx-signal-store\/\">Using Angular&#8217;s Resource API with the NGRX Signal Store<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/the-new-event-api-for-the-ngrx-signal-store-a-quantum-of-redux\/\">The new Event API for the NgRx Signal Store: A Quantum of Redux<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/full-cycle-reativity-in-angular-signal-forms-signal-store-resources-mutation-api\/\">Full-Cycle Reactivity in Angular: Signal Forms, Signal Store, Resources, &#038; Mutation API<\/a><\/li>\n\t\t\t\t\t\t\t<\/ol>\n\t\t<\/div>\n\t<\/div>\n<p>The NGRX Signal Store, released shortly after Angular 17, offers a very lightweight solution for state management. With no direct dependencies on RxJS, it relies entirely on Signals. However, its greatest strength is undoubtedly its high degree of expandability. With so-called custom features, recurring tasks can be implemented very easily in a central way.<\/p>\n<p>The first example of custom features presented here is very straightforward. After that, things get a little more challenging: The consumer of a feature must be able to determine the names of the signals and methods set up by the feature. Otherwise, naming conflicts will arise quickly. As the following examples show, this does not contradict strict typing in TypeScript.<\/p>\n<p>The examples shown here are inspired by an example from <a href=\"https:\/\/twitter.com\/MarkoStDev\">Marko Stanimirovi\u0107<\/a>, the NGRX core team member behind the Signal Store, and the Entity management solution <code>@ngrx\/signals\/entity<\/code> shipped with the Signal Store.<\/p>\n<p>\ud83d\udcc2 <a href=\"https:\/\/github.com\/manfredsteyer\/standalone-example-cli\/tree\/arc-signal-store-custom-typed\">Source Code<\/a> (Branch: arc-signal-store-custom-typed)<\/p>\n<h2>Signal Store 101<\/h2>\n<p>Before we start, I would like to quickly get everyone on board and summarize the most important aspects of the NGRX Signal Store. A <a href=\"https:\/\/www.angulararchitects.io\/blog\/the-new-ngrx-signal-store-for-angular-2-1-flavors\/\">detailed overview<\/a> can be found in <a href=\"https:\/\/www.angulararchitects.io\/blog\/the-new-ngrx-signal-store-for-angular-2-1-flavors\/\">my article here<\/a>.<\/p>\n<p>The signal store comes with NGRX since version 17 and is located in the npm package <code>@ngrx\/signals<\/code>. At the center is the <code>signalStore<\/code> function, which creates a new Signal Store. Technically, this is an Angular service that receives signals, derived (calculated) signals, and methods for state management:<\/p>\n<pre><code class=\"language-typescript\">import {\n  patchState,\n  signalStore,\n  withHooks,\n  withMethods,\n  withComputed,\n  withState,\n} from &#039;@ngrx\/signals&#039;;\n\n[\u2026]\n\nexport const FlightBookingStore = signalStore(\n\n  { providedIn: &#039;root&#039; },\n\n  \/\/ State Properties\n  withState({\n    from: &#039;Paris&#039;,\n    to: &#039;London&#039;,\n    initialized: false,\n    flights: [] as Flight[],\n    basket: {} as Record&lt;number, boolean&gt;,\n  }),\n\n  \/\/ Calculated State\n  withComputed(({ flights, basket, from, to }) =&gt; ({\n    selected: computed(() =&gt; flights().filter((f) =&gt; basket()[f.id])),\n    criteria: computed(() =&gt; ({ from: from(), to: to() })),\n  })),\n\n  \/\/ Extension\n  withCallState(),\n\n  \/\/ Methods\n  withMethods((state) =&gt; {\n    const { basket, flights, from, to, initialized } = state;\n    const flightService = inject(FlightService);\n\n    return {\n      load: async () =&gt; {\n        if (!from() || !to()) return;\n\n        \/\/ Updating the extension\u2019s state (callState)\n        patchState(state, setLoading());\n\n        const flights = await flightService.findPromise(from(), to());\n        patchState(state, { flights });\n\n        \/\/ Updating the extension\u2019s state (callState)\n        patchState(state, setLoaded());\n\n      },\n    };\n\n  })\n);<\/code><\/pre>\n<p>Interestingly, the Signal Store is already based entirely on so-called features in this first expansion stage: <code>withState<\/code> , <code>withComputed<\/code> or <code>withMethods<\/code> are three features that enrich the store with properties (signals and calculated signals) and methods.<\/p>\n<p>The function <code>withCallState<\/code> is a first simple custom feature that introduces a <code>callState<\/code> Signal. This signal informs, among other things, whether data is currently being loaded or whether the loading process has already been completed. It introduces the following signals:<\/p>\n<ul>\n<li><code>callState<\/code>: Can hold the values <code>init<\/code>, <code>loading<\/code>, <code>loaded,<\/code> or an object with an error message.<\/li>\n<li><code>loading<\/code>: Computed signal; returns <code>true<\/code> if the <code>callState<\/code> has the value <code>loading<\/code>.<\/li>\n<li><code>loaded<\/code>: Calculated signal; returns <code>true<\/code> if the <code>callState<\/code> has the value <code>loaded<\/code>.<\/li>\n<li><code>error<\/code>: Calculated signal; returns an error message that may be stored in the <code>callState<\/code> or <code>null<\/code>.<\/li>\n<\/ul>\n<p>The <code>CallState<\/code> feature also offers so-called Updaters such as <code>setLoading<\/code> or <code>setLoaded<\/code>. These are functions used by consumers like Listing 1 for changing the value of the <code>callState.<\/code> In general,Updaters allow consumers to change the managed state without having to know exactly how it is structured.<\/p>\n<p>The next listing shows a simple component that uses the <code>FlightBookingStore<\/code>. It delegates to the individual Signals and to its <code>load<\/code> method. It also accesses the <code>loading<\/code> signal set up by the <code>CallState<\/code> feature:<\/p>\n<pre><code class=\"language-typescript\">@Component({ \u2026 })\nexport class FlightSearchComponent {\n  private store = inject(FlightBookingStore);\n\n  \/\/ Getting Signals from Store\n  from = this.store.from;\n  to = this.store.to;\n  basket = this.store.basket;\n  flights = this.store.flights;\n  selected = this.store.selected;\n\n  \/\/ Getting Signal from Extension\n  loading = this.store.loading;\n\n  async search() {\n    this.store.load();\n  }\n}<\/code><\/pre>\n<h2>A Simple First Extension<\/h2>\n<p>Here is a first, very simple version of the <code>CallState<\/code> feature used in the last section:<\/p>\n<pre><code class=\"language-typescript\">import {\n  SignalStoreFeature,\n  signalStoreFeature,\n  withComputed,\n  withState,\n} from &#039;@ngrx\/signals&#039;;\n\n[\u2026]\n\nexport type CallState = &#039;init&#039; | &#039;loading&#039; | &#039;loaded&#039; | { error: string };\n\nexport function withCallState() {\n  return signalStoreFeature(\n    withState&lt;{ callState: CallState }&gt;({ callState: &#039;init&#039; }),\n    withComputed(({ callState }) =&gt; ({\n      loading: computed(() =&gt; callState() === &#039;loading&#039;),\n      loaded: computed(() =&gt; callState() === &#039;loaded&#039;),\n      error: computed(() =&gt; {\n        const state = callState();\n        return typeof state === &#039;object&#039; ? state.error : null\n      }),\n    }))\n  );\n}<\/code><\/pre>\n<p>This is a function that returns the result of <code>signalStoreFeature<\/code>. The <code>signalStoreFeature<\/code> function, in turn, simply groups existing features: <code>withState<\/code> introduces the <code>callState<\/code> property, and <code>withComputed<\/code> defines the previously discussed calculated signals based on it.<\/p>\n<p>The Updaters provided by the feature only return a partial state object with the property to be updated:<\/p>\n<pre><code class=\"language-typescript\">export function setLoading(): { callState: CallState } {\n  return { callState: &#039;loading&#039; };\n}\n\nexport function setLoaded(): { callState: CallState } {\n  return { callState: &#039;loaded&#039; };\n}   \n\nexport function setError(error: string): { callState: CallState } {\n  return { callState: { error } };\n}<\/code><\/pre>\n<h2>Now it Really Starts: Typing<\/h2>\n<p>The <code>CallState<\/code> implementation in the last section briefly summarizes the solution to a recurring requirement. Once implemented, individual applications can integrate the feature into their stores.<\/p>\n<p>A drawback of this implementation, however, is that the signals introduced have fixed names: <code>callState<\/code>, <code>loading<\/code>, <code>loaded,<\/code> and <code>error<\/code>. This quickly leads to naming conflicts, especially if the same store requires the feature more than once. An example of this is a store that wants to manage several <code>callStates<\/code> for different entities, e.g. for flights and passengers.<\/p>\n<p>In this case, the consumer should be able to specify the names of the signals introduced. That's precisely what we'll take care of below. To make this extension type-safe, we first have to think a little about the typing of the <code>withCallState<\/code> function.<\/p>\n<p>Our <code>withCallState<\/code> function does not currently have an explicit return type. Therefore, TypeScript infers this type by looking at the return value in the function. The compiler realizes that a <code>callState<\/code> property is available.<\/p>\n<p>The type determined here by inference is a <code>SignalStoreFeature&lt;Input, Output&gt;<\/code>. The type parameter <code>Input<\/code> defines which signals and methods the feature expects from the store, and <code>Output<\/code> specifies which additional signals and methods the feature provides. Our feature does not place any expectations on the store, but provides a <code>callState<\/code> signal as well as several calculated signals such as <code>loading<\/code>. Respectively, our <code>Input<\/code> and <code>Output<\/code> types looks as follows:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/extensions-type-system-01.png\" alt=\"Custom Extensions from the type system perspective\" \/><\/p>\n<p>It should be noted that <code>state<\/code> describes the signal to be introduced, and the <code>signals<\/code> property represents the signals calculated from it. This representation at least corresponds to the simplified external view.<\/p>\n<p>The internal view is a little more complex, especially since <code>withState<\/code> first introduces the <code>callState<\/code> signal and only then <code>withComputed<\/code> adds the calculated signals. That's why the inside view has two outputs, which are combined using a helper type.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/extensions-type-system-02.png\" alt=\"Custom Extensions from the type system perspective\" \/><\/p>\n<p>For the sake of simplicity, the previous image calls the helper type <code>Merged Result<\/code>. However, the truth is that the Signal Store has several internal types for this.<\/p>\n<p>On a logical level, the the internal view und the external one are equivalent. TypeScript may need a little nudge in the form of a type assertion to recognize this. However, explicitly defining the internal view is a bit annoying and currently not really possible because the required helper types are not part of the Signal Store's public API. That's why I 'm using a pattern here that can also be found several times in the Signal Store code: A combination of a function overload with the external view and a function implementation that uses <code>SignalStoreFeature<\/code> instead of <code>SignalStoreFeature&lt;Input, Output&gt;<\/code> for the internal view:<\/p>\n<pre><code class=\"language-typescript\">\/\/ Overloading with External View\nexport function withCallState()\n  : SignalStoreFeature&lt;\n    {\n      state: {},\n      signals: {},\n      methods: {}\n    },\n    {\n      state: {\n        callState: CallState\n      },\n      signals: {\n        loading: Signal&lt;boolean&gt;,\n        loaded: Signal&lt;boolean&gt;,\n        error: Signal&lt;{ error: string } | null&gt;\n      },\n      methods: {}\n    }&gt;;\n\/\/ Implementation with Internal View\nexport function withCallState(): SignalStoreFeature {\n  return signalStoreFeature(\n    withState&lt;{ callState: CallState }&gt;({ callState: &#039;init&#039; }),\n    withComputed(({ callState }) =&gt; ({\n      loading: computed(() =&gt; callState() === &#039;loading&#039;),\n      loaded: computed(() =&gt; callState() === &#039;loaded&#039;),\n      error: computed(() =&gt; {\n        const state = callState();\n        return typeof state === &#039;object&#039; ? state.error : null\n      }),\n    }))\n  );\n}<\/code><\/pre>\n<p>The <code>SignalStoreFeature<\/code> type without type parameters uses more general types for <code>Input<\/code> and <code>Output<\/code> that do not assume specific names or data types.<\/p>\n<h2>More: Angular Architecture Workshop (online, interactive, advanced)<\/h2>\n<p>Become an expert for enterprise-scale and maintainable Angular applications with our <a href=\"https:\/\/www.angulararchitects.io\/en\/angular-workshops\/advanced-angular-enterprise-architecture-incl-ivy\/\">Angular Architecture workshop!<\/a><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2020\/06\/ent-en-1.jpg\" alt=\"\" \/><\/p>\n<p><a href=\"https:\/\/www.angulararchitects.io\/en\/angular-workshops\/advanced-angular-enterprise-architecture-incl-ivy\/\">All Details (English Workshop)<\/a> | <a href=\"https:\/\/www.angulararchitects.io\/schulungen\/advanced-angular-enterprise-anwendungen-und-architektur\/\">All Details (German Workshop)<\/a><br \/>\n<\/p>\n<h2>Typing and Dynamic Properties \u2013 How do They Work Together?<\/h2>\n<p>Now that the basic structure of the typing is in place, we can extend it with configurable property names. Following the example of <code>@ngrx\/signals\/entity<\/code>, consumers should have the option to define a prefix when activating the feature:<\/p>\n<pre><code class=\"language-typescript\">export const FlightBookingStore = signalStore(\n  { providedIn: &#039;root&#039; },\n\n  withState({ \u2026 }),\n  withComputed(( \u2026 ) =&gt; ({ \u2026 })),\n\n  withCallState({ prop: &#039;flights&#039; }), \n  withCallState({ prop: &#039;passengers&#039;}),\n\n  [\u2026]\n);<\/code><\/pre>\n<p>This prefix should now be included in the property names defined by the feature. For example, the first call to <code>withCallState<\/code> should produce the following properties:<\/p>\n<ul>\n<li><code>flightsCallState<\/code> (state)<\/li>\n<li><code>flightsLoading<\/code> (computed)<\/li>\n<li><code>flightsLoaded<\/code> (computed)<\/li>\n<li><code>flightsError<\/code> (computed)<\/li>\n<\/ul>\n<p>The second call analogously leads to these properties:<\/p>\n<ul>\n<li><code>passengersCallState<\/code> (state)<\/li>\n<li><code>passengersLoading<\/code> (computed)<\/li>\n<li><code>passengersLoaded<\/code> (computed)<\/li>\n<li><code>passengersError<\/code> (computed)<\/li>\n<\/ul>\n<p>Setting up these properties at runtime isn't a big problem in the world of TypeScript, especially since the underlying JavaScript is a dynamic language anyway. The challenge, however, is to also inform the type system about these properties.<\/p>\n<p>For this task, you first need to find a way to express the prefix in a type declaration. At this point, we benefit from the fact that literals can also be used as data types:<\/p>\n<pre><code class=\"language-typescript\">export type BoxStatus = &#039;open&#039; | &#039;closed&#039;;\nconst candyBox: BoxStatus = &#039;open&#039;;<\/code><\/pre>\n<p>Such String Literal Union Types are often used in TypeScript applications to express enums. This is even closer to EcmaScript than using TypeScript's <code>enum<\/code> keyword. Funnily, nobody is forcing us to offer multiple options. That's why this variant is completely ok:<\/p>\n<pre><code class=\"language-typescript\">export type BoxStatusAfterHolidays = &#039;closed&#039;;<\/code><\/pre>\n<p>So here we have a type that can hold exactly a single string value. We use this exact pattern to inform the type system about our prefix. First, we create a type that defines the name of the signal to be introduced based on the prefix:<\/p>\n<pre><code class=\"language-typescript\">export type NamedCallState&lt;Prop extends string&gt; = {\n  [K in Prop as `${K}CallState`]: CallState;\n};<\/code><\/pre>\n<p>This is a so-called mapped type, which maps one type to a new one. The type parameter <code>Prop extends string<\/code> describes the original type. It can be any string used as a type. String must also be written in lowercase because, at this point, we are referring to a specific string and not the <code>String object type.<\/code> The notation <code>K in Prop<\/code> also reduces to this string. In more complex cases, one could use the keyword <code>in<\/code>, for instance, to loop through the properties of the original type.<\/p>\n<p>We can proceed analogously for the calculated signals to be introduced:<\/p>\n<pre><code class=\"language-typescript\">export type NamedCallStateComputed&lt;Prop extends string&gt; = {\n  [K in Prop as `${K}Loading`]: Signal&lt;boolean&gt;;\n} &amp; {\n    [K in Prop as `${K}Loaded`]: Signal&lt;boolean&gt;;\n} &amp; {\n    [K in Prop as `${K}Error`]: Signal&lt;string | null&gt;;\n};<\/code><\/pre>\n<p>Since a mapped type can only have a single mapping, several mapped types are used here. They are combined with the <code>&amp;<\/code>-operator (intersection operator). With these two types we can now specify the typing of our <code>withCallState<\/code> function:<\/p>\n<pre><code class=\"language-typescript\">export function withCallState&lt;Prop extends string&gt;(config: {\n  prop: Prop;\n}): SignalStoreFeature&lt;\n  { state: {}, signals: {}, methods: {} },\n  {\n    state: NamedCallState&lt;Prop&gt;,\n    signals: NamedCallStateComputed&lt;Prop&gt;,\n    methods: {}\n  }\n>;\nexport function withCallState&lt;Prop extends string&gt;(config: {\n  prop: Prop;\n}): SignalStoreFeature {\n[\u2026]\n}<\/code><\/pre>\n<p>Now, the type system knows about our configured properties. In addition, it is now important to set up these properties at runtime. An auxiliary function <code>getCallStateKeys<\/code> is used for this purpose:<\/p>\n<pre><code class=\"language-typescript\">function getCallStateKeys(config: { prop: string }) {\n  return {\n    callStateKey: `${config.prop}CallState`,\n    loadingKey: `${config.prop}Loading`,\n    loadedKey: `${config.prop}Loaded`,\n    errorKey: `${config.prop}Error`,\n  };\n}<\/code><\/pre>\n<p>This helper function returns the same mappings at runtime as the previously introduced types during compile time. The updated implementation of <code>withCallState<\/code> picks up these names and sets up corresponding properties:<\/p>\n<pre><code class=\"language-typescript\">[\u2026]\nexport function withCallState&lt;Prop extends string&gt;(config: {\n  prop: Prop;\n}): SignalStoreFeature {\n  const { callStateKey, errorKey, loadedKey, loadingKey } =\n    getCallStateKeys(config);\n\n  return signalStoreFeature(\n    withState({ [callStateKey]: &#039;init&#039; }),\n    withComputed((state: Record&lt;string, Signal&lt;unknown&gt;&gt;) =&gt; {\n\n      const callState = state[callStateKey] as Signal&lt;CallState&gt;;\n\n      return {\n        [loadingKey]: computed(() =&gt; callState() === &#039;loading&#039;),\n        [loadedKey]: computed(() =&gt; callState() === &#039;loaded&#039;),\n        [errorKey]: computed(() =&gt; {\n          const v = callState();\n          return typeof v === &#039;object&#039; ? v.error : null;\n        })\n      }\n    })\n  );\n}<\/code><\/pre>\n<p>So that the updaters can cope with the dynamic properties, they also receive a corresponding parameter:<\/p>\n<pre><code class=\"language-typescript\">export function setLoading&lt;Prop extends string&gt;(\n  prop: Prop\n): NamedCallState&lt;Prop&gt; {\n  return { [`${prop}CallState`]: &#039;loading&#039; } as NamedCallState&lt;Prop&gt;;\n}<\/code><\/pre>\n<p>This idea can also be found in <code>@ngrx\/signals\/entity<\/code>. The updater is then used as follows:<\/p>\n<pre><code class=\"language-typescript\">load: async () =&gt; {\n  patchState(state, setLoading(&#039;flights&#039;));\n  [\u2026]\n}<\/code><\/pre>\n<h2>More Examples: CRUD and Undo\/Redo<\/h2>\n<p>In the previous article, I demonstrated features for implementing CRUD and Undo\/Redo. The following repo contains a version of these custom features using dynamic properties as shown here.<\/p>\n<p>\ud83d\udcc2 <a href=\"https:\/\/github.com\/manfredsteyer\/standalone-example-cli\/tree\/arc-signal-store-custom-examples-typed\">Source Code<\/a> (see \ud83d\udd00 branch <code>arc-signal-store-custom-examples-typed<\/code>)<\/p>\n<h2>Conclusion<\/h2>\n<p>The NGRX team is known for being exceptionally skilled at leveraging the possibilities of the TypeScript type system. The result is an extremely easy-to-use and type-safe API. <\/p>\n<p>In this article, we switched perspectives and discussed how you can leverage the patterns used by the NGRX team for your custom Signal Store features. This enables to configure property names and thus avoid naming conflicts without compromising type safety.<\/p>\n<p>To do this, we have to deal with aspects of TypeScript that application developers usually don't get in contact that often. That's why the patterns used may sometimes seem a bit complicated. The good news is that we only need these patterns if we are developing highly reusable solutions. As soon as we switch back to the role of application developer, we have a type-safe solution that is comfortable to use.<\/p>\n<h2>What's next? More on Architecture!<\/h2>\n<p>Please find more information on enterprise-scale Angular architectures in our free eBook (5th edition, 12 chapters):<\/p>\n<ul>\n<li>According to which criteria can we subdivide a huge application into sub-domains?<\/li>\n<li>How can we make sure, the solution is maintainable for years or even decades?<\/li>\n<li>Which options from Micro Frontends are provided by Module Federation?<\/li>\n<\/ul>\n<p><a href=\"https:\/\/www.angulararchitects.io\/book\"><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2022\/12\/cover-5th-small.png\" alt=\"free\" \/><\/a><\/p>\n<p>Feel free to <a href=\"https:\/\/www.angulararchitects.io\/en\/book\">download it here<\/a> now!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is post 3 of 7 in the series &ldquo;NGRX Signal Store&rdquo; The new NGRX Signal Store for Angular: 3+n Flavors Smarter, Not Harder: Simplifying your Application With NGRX Signal Store and Custom Features NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions The NGRX Signal Store and Your Architecture Using Angular&#8217;s Resource API [&hellip;]<\/p>\n","protected":false},"author":25,"featured_media":23539,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_price":"","_stock":"","_tribe_ticket_header":"","_tribe_default_ticket_provider":"","_ticket_start_date":"","_ticket_end_date":"","_tribe_ticket_show_description":"","_tribe_ticket_show_not_going":false,"_tribe_ticket_use_global_stock":"","_tribe_ticket_global_stock_level":"","_global_stock_mode":"","_global_stock_cap":"","_tribe_rsvp_for_event":"","_tribe_ticket_going_count":"","_tribe_ticket_not_going_count":"","_tribe_tickets_list":"[]","_tribe_ticket_has_attendee_info_fields":false,"footnotes":""},"categories":[18],"tags":[],"class_list":["post-23543","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","post_series-ngrx-signal-store-en"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.1.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions - ANGULARarchitects<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions - ANGULARarchitects\" \/>\n<meta property=\"og:description\" content=\"This is post 3 of 7 in the series &ldquo;NGRX Signal Store&rdquo; The new NGRX Signal Store for Angular: 3+n Flavors Smarter, Not Harder: Simplifying your Application With NGRX Signal Store and Custom Features NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions The NGRX Signal Store and Your Architecture Using Angular&#8217;s Resource API [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\" \/>\n<meta property=\"og:site_name\" content=\"ANGULARarchitects\" \/>\n<meta property=\"article:published_time\" content=\"2023-11-26T23:08:37+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2023-12-11T17:01:26+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/sujet-1.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1000\" \/>\n\t<meta property=\"og:image:height\" content=\"500\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Manfred Steyer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/sujet-1.jpg\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Manfred Steyer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\"},\"author\":{\"name\":\"Manfred Steyer\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a\"},\"headline\":\"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions\",\"datePublished\":\"2023-11-26T23:08:37+00:00\",\"dateModified\":\"2023-12-11T17:01:26+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\"},\"wordCount\":1659,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\",\"name\":\"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions - ANGULARarchitects\",\"isPartOf\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg\",\"datePublished\":\"2023-11-26T23:08:37+00:00\",\"dateModified\":\"2023-12-11T17:01:26+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage\",\"url\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg\",\"contentUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg\",\"width\":1000,\"height\":667},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.angulararchitects.io\/en\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#website\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/\",\"name\":\"ANGULARarchitects\",\"description\":\"AngularArchitects.io\",\"publisher\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.angulararchitects.io\/en\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\",\"name\":\"ANGULARarchitects\",\"alternateName\":\"SOFTWAREarchitects\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg\",\"contentUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg\",\"width\":644,\"height\":216,\"caption\":\"ANGULARarchitects\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/github.com\/angular-architects\",\"https:\/\/www.linkedin.com\/company\/angular-architects\/\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a\",\"name\":\"Manfred Steyer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g\",\"caption\":\"Manfred Steyer\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions - ANGULARarchitects","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/","og_locale":"en_US","og_type":"article","og_title":"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions - ANGULARarchitects","og_description":"This is post 3 of 7 in the series &ldquo;NGRX Signal Store&rdquo; The new NGRX Signal Store for Angular: 3+n Flavors Smarter, Not Harder: Simplifying your Application With NGRX Signal Store and Custom Features NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions The NGRX Signal Store and Your Architecture Using Angular&#8217;s Resource API [&hellip;]","og_url":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/","og_site_name":"ANGULARarchitects","article_published_time":"2023-11-26T23:08:37+00:00","article_modified_time":"2023-12-11T17:01:26+00:00","og_image":[{"width":1000,"height":500,"url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/sujet-1.jpg","type":"image\/jpeg"}],"author":"Manfred Steyer","twitter_card":"summary_large_image","twitter_image":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/sujet-1.jpg","twitter_misc":{"Written by":"Manfred Steyer","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#article","isPartOf":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/"},"author":{"name":"Manfred Steyer","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a"},"headline":"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions","datePublished":"2023-11-26T23:08:37+00:00","dateModified":"2023-12-11T17:01:26+00:00","mainEntityOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/"},"wordCount":1659,"commentCount":0,"publisher":{"@id":"https:\/\/www.angulararchitects.io\/en\/#organization"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage"},"thumbnailUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg","inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/","url":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/","name":"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions - ANGULARarchitects","isPartOf":{"@id":"https:\/\/www.angulararchitects.io\/en\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage"},"thumbnailUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg","datePublished":"2023-11-26T23:08:37+00:00","dateModified":"2023-12-11T17:01:26+00:00","breadcrumb":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#primaryimage","url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg","contentUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/11\/shutterstock_1500830729.jpg","width":1000,"height":667},{"@type":"BreadcrumbList","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/ngrx-signal-store-deep-dive-flexible-and-type-safe-custom-extensions\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.angulararchitects.io\/en\/"},{"@type":"ListItem","position":2,"name":"NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions"}]},{"@type":"WebSite","@id":"https:\/\/www.angulararchitects.io\/en\/#website","url":"https:\/\/www.angulararchitects.io\/en\/","name":"ANGULARarchitects","description":"AngularArchitects.io","publisher":{"@id":"https:\/\/www.angulararchitects.io\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.angulararchitects.io\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.angulararchitects.io\/en\/#organization","name":"ANGULARarchitects","alternateName":"SOFTWAREarchitects","url":"https:\/\/www.angulararchitects.io\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/","url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg","contentUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg","width":644,"height":216,"caption":"ANGULARarchitects"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/github.com\/angular-architects","https:\/\/www.linkedin.com\/company\/angular-architects\/"]},{"@type":"Person","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a","name":"Manfred Steyer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g","caption":"Manfred Steyer"}}]}},"_links":{"self":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/23543","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/users\/25"}],"replies":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/comments?post=23543"}],"version-history":[{"count":10,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/23543\/revisions"}],"predecessor-version":[{"id":23771,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/23543\/revisions\/23771"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/media\/23539"}],"wp:attachment":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/media?parent=23543"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/categories?post=23543"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/tags?post=23543"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}