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

Generic type argument for QueryParamGroup #91

Open
Airblader opened this issue Feb 18, 2019 · 2 comments
Open

Generic type argument for QueryParamGroup #91

Airblader opened this issue Feb 18, 2019 · 2 comments
Labels
Comp: Core Core functionality of ngqp Priority: Low This is nice-to-have, but doesn't really affect users directly Status: Discussion We're not yet sure whether or how to implement this feature Type: Feature If you want to add something that doesn't exist yet

Comments

@Airblader
Copy link
Collaborator

What's your idea?

It would be great if QueryParamGroup was generic (in terms of the names of the parameters) so that value, valueChanges, setValue and patchValue could be typed better.

I've attempted this in the early days and recall that it wasn't quite that easy, but we should attempt this again.

@Airblader Airblader added Type: Feature If you want to add something that doesn't exist yet Status: Accepted This issue has been approved and PRs will be accepted Comp: Core Core functionality of ngqp Priority: Low This is nice-to-have, but doesn't really affect users directly labels Feb 18, 2019
@Airblader
Copy link
Collaborator Author

I've been playing around with this. It's… really tricky. Christ! So this version (as of commit c762650) compiles. But plenty of work left to be done here, and it gets ugly. :-/

diff --git a/projects/ngqp/core/src/lib/directives/query-param-group.service.ts b/projects/ngqp/core/src/lib/directives/query-param-group.service.ts
index 80540d3..53e05bd 100644
--- a/projects/ngqp/core/src/lib/directives/query-param-group.service.ts
+++ b/projects/ngqp/core/src/lib/directives/query-param-group.service.ts
@@ -42,7 +42,7 @@ class NavigationData {
 export class QueryParamGroupService implements OnDestroy {
 
     /** The {@link QueryParamGroup} to bind. */
-    private queryParamGroup: QueryParamGroup;
+    private queryParamGroup: QueryParamGroup<any, any>;
 
     /** List of {@link QueryParamAccessor} registered to this service. */
     private directives = new Map<string, QueryParamAccessor[]>();
@@ -83,7 +83,7 @@ export class QueryParamGroupService implements OnDestroy {
     /**
      * Uses the given {@link QueryParamGroup} for synchronization.
      */
-    public setQueryParamGroup(queryParamGroup: QueryParamGroup): void {
+    public setQueryParamGroup(queryParamGroup: QueryParamGroup<any, any>): void {
         // FIXME: If this is called when we already have a group, we probably need to do
         //        some cleanup first.
         if (this.queryParamGroup) {
@@ -308,7 +308,7 @@ export class QueryParamGroupService implements OnDestroy {
      * This consists mainly of properly serializing the model value and ensuring to take
      * side effect changes into account that may have been configured.
      */
-    private getParamsForValue(queryParam: QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown>, value: any): Params {
+    private getParamsForValue(queryParam: QueryParam<any> | MultiQueryParam<any> | PartitionedQueryParam<any>, value: any): Params {
         const partitionedQueryParam = this.wrapIntoPartition(queryParam);
         const partitioned = partitionedQueryParam.partition(value);
 
@@ -370,8 +370,8 @@ export class QueryParamGroupService implements OnDestroy {
      * Wraps a query parameter into a partition if it isn't already.
      */
     private wrapIntoPartition(
-        queryParam: QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown>
-    ): PartitionedQueryParam<unknown> {
+        queryParam: QueryParam<any> | MultiQueryParam<any> | PartitionedQueryParam<any>
+    ): PartitionedQueryParam<any> {
         if (queryParam instanceof PartitionedQueryParam) {
             return queryParam;
         }
diff --git a/projects/ngqp/core/src/lib/directives/query-param.directive.ts b/projects/ngqp/core/src/lib/directives/query-param.directive.ts
index 1e0111b..3998ae6 100644
--- a/projects/ngqp/core/src/lib/directives/query-param.directive.ts
+++ b/projects/ngqp/core/src/lib/directives/query-param.directive.ts
@@ -32,7 +32,7 @@ export class QueryParamDirective implements QueryParamAccessor, OnChanges, OnDes
     public valueAccessor: ControlValueAccessor | null = null;
 
     /** @internal */
-    private group = new QueryParamGroup({});
+    private group = new QueryParamGroup<'param', any>({});
 
     /** @internal */
     constructor(
diff --git a/projects/ngqp/core/src/lib/model/query-param-group.ts b/projects/ngqp/core/src/lib/model/query-param-group.ts
index b5d056e..7639c0c 100644
--- a/projects/ngqp/core/src/lib/model/query-param-group.ts
+++ b/projects/ngqp/core/src/lib/model/query-param-group.ts
@@ -1,8 +1,10 @@
 import { Observable, Subject } from 'rxjs';
 import { isMissing, undefinedToNull } from '../util';
 import { OnChangeFunction } from '../types';
-import { MultiQueryParam, QueryParam, PartitionedQueryParam } from './query-param';
 import { RouterOptions } from '../router-adapter/router-adapter.interface';
+import { MultiQueryParam, QueryParam, PartitionedQueryParam, GroupToParams, GroupKeyToParam } from './query-param';
+
+export const keys = Object.keys as <T>(o: T) => (keyof T)[];
 
 /**
  * Groups multiple {@link QueryParam} instances to a single unit.
@@ -11,10 +13,10 @@ import { RouterOptions } from '../router-adapter/router-adapter.interface';
  * complete unit. Collecting parameters into a group is required for the synchronization
  * to and from the URL.
  */
-export class QueryParamGroup {
+export class QueryParamGroup<K extends string = string, G extends { [Key in K]: unknown } = any> {
 
     /** @internal */
-    private readonly _valueChanges = new Subject<Record<string, any>>();
+    private readonly _valueChanges = new Subject<G>();
 
     /**
      * Emits the values of all parameters in this group whenever at least one changes.
@@ -25,34 +27,26 @@ export class QueryParamGroup {
      *
      * NOTE: This observable does not complete on its own, so ensure to unsubscribe from it.
      */
-    public readonly valueChanges: Observable<Record<string, any>> = this._valueChanges.asObservable();
-
-    /** @internal */
-    private readonly _queryParamAdded$ = new Subject<string>();
+    public readonly valueChanges: Observable<G> = this._valueChanges.asObservable();
 
     /** @internal */
-    public readonly queryParamAdded$: Observable<string> = this._queryParamAdded$.asObservable();
+    private readonly _queryParamAdded$ = new Subject<K>();
 
     /** @internal */
-    public readonly queryParams: { [ queryParamName: string ]: QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown> };
+    public readonly queryParamAdded$: Observable<K> = this._queryParamAdded$.asObservable();
 
-    /** @internal */
-    public readonly routerOptions: RouterOptions;
-
-    private changeFunctions: OnChangeFunction<Record<string, any>>[] = [];
+    private changeFunctions: OnChangeFunction<G>[] = [];
 
     constructor(
-        queryParams: { [ queryParamName: string ]: QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown> },
-        extras: RouterOptions = {}
+        public readonly queryParams: GroupToParams<K, G>,
+        public readonly routerOptions: RouterOptions = {}
     ) {
-        this.queryParams = queryParams;
-        this.routerOptions = extras;
-
-        Object.values(this.queryParams).forEach(queryParam => queryParam._setParent(this));
+        keys(this.queryParams)
+            .forEach(queryParamName => this.queryParams[ queryParamName ]._setParent(this as any));
     }
 
     /** @internal */
-    public _registerOnChange(fn: OnChangeFunction<Record<string, any>>): void {
+    public _registerOnChange(fn: OnChangeFunction<G>): void {
         this.changeFunctions.push(fn);
     }
 
@@ -70,7 +64,9 @@ export class QueryParamGroup {
      *
      * @param queryParamName The name of the parameter instance to retrieve.
      */
-    public get(queryParamName: string): QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown> | null {
+    public get<Key extends K>(
+        queryParamName: Key
+    ): GroupKeyToParam<K, G, Key> | null {
         const param = this.queryParams[ queryParamName ];
         if (!param) {
             return null;
@@ -89,13 +85,16 @@ export class QueryParamGroup {
      * @param queryParamName Name of the parameter to reference it with.
      * @param queryParam The new parameter to add.
      */
-    public add(queryParamName: string, queryParam: QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown>): void {
+    public add<Key extends K>(
+        queryParamName: Key,
+        queryParam: GroupKeyToParam<K, G, Key>
+    ): void {
         if (this.get(queryParamName)) {
             throw new Error(`A parameter with name ${queryParamName} already exists.`);
         }
 
         this.queryParams[ queryParamName ] = queryParam;
-        queryParam._setParent(this);
+        queryParam._setParent(this as any);
         this._queryParamAdded$.next(queryParamName);
     }
 
@@ -108,7 +107,7 @@ export class QueryParamGroup {
      *
      * @param queryParamName The name of the parameter to remove.
      */
-    public remove(queryParamName: string): void {
+    public remove<Key extends K>(queryParamName: Key): void {
         const queryParam = this.get(queryParamName);
         if (!queryParam) {
             throw new Error(`No parameter with name ${queryParamName} found.`);
@@ -125,9 +124,10 @@ export class QueryParamGroup {
      * See {@link QueryParamGroup#valueChanges} for a description of the format of
      * the value.
      */
-    public get value(): Record<string, any> {
-        const value: Record<string, any> = {};
-        Object.keys(this.queryParams).forEach(queryParamName => value[ queryParamName ] = this.queryParams[ queryParamName ].value);
+    public get value(): G {
+        const value = {} as G;
+        keys(this.queryParams)
+            .forEach(queryParamName => value[ queryParamName ] = this.queryParams[ queryParamName ].value);
 
         return value;
     }
@@ -141,17 +141,17 @@ export class QueryParamGroup {
      * @param value See {@link QueryParamGroup#valueChanges} for a description of the format.
      * @param opts Additional options
      */
-    public patchValue(value: Record<string, any>, opts: {
+    public patchValue(value: Partial<G>, opts: {
         emitEvent?: boolean,
         emitModelToViewChange?: boolean,
     } = {}): void {
-        Object.keys(value).forEach(queryParamName => {
-            const queryParam = this.queryParams[ queryParamName ];
+        keys(value).forEach(queryParamName => {
+            const queryParam = this.queryParams[ queryParamName as K ];
             if (isMissing(queryParam)) {
                 return;
             }
 
-            queryParam.setValue(value[ queryParamName ], {
+            queryParam.setValue(value[ queryParamName ] as any, {
                 emitEvent: opts.emitEvent,
                 onlySelf: true,
                 emitModelToViewChange: false,
@@ -170,12 +170,12 @@ export class QueryParamGroup {
      * @param value See {@link QueryParamGroup#valueChanges} for a description of the format.
      * @param opts Additional options
      */
-    public setValue(value: Record<string, any>, opts: {
+    public setValue(value: G, opts: {
         emitEvent?: boolean,
         emitModelToViewChange?: boolean,
     } = {}): void {
-        Object.keys(this.queryParams).forEach(queryParamName => {
-            this.queryParams[ queryParamName ].setValue(undefinedToNull(value[ queryParamName ]), {
+        keys(this.queryParams).forEach(queryParamName => {
+            this.queryParams[ queryParamName ].setValue(undefinedToNull(value[ queryParamName ] as any), {
                 emitEvent: opts.emitEvent,
                 onlySelf: true,
                 emitModelToViewChange: false,
diff --git a/projects/ngqp/core/src/lib/model/query-param.ts b/projects/ngqp/core/src/lib/model/query-param.ts
index a32111c..4755567 100644
--- a/projects/ngqp/core/src/lib/model/query-param.ts
+++ b/projects/ngqp/core/src/lib/model/query-param.ts
@@ -288,4 +288,12 @@ export class PartitionedQueryParam<T, G extends unknown[] = unknown[]> extends A
         }
     }
 
-}
\ No newline at end of file
+}
+
+export type GroupKeyToParam<K extends string, G extends { [P in K]: unknown }, Key extends K> = G[Key] extends (infer U)[]
+    ? (QueryParam<G[Key]> | MultiQueryParam<U> | PartitionedQueryParam<G[Key]>)
+    : QueryParam<G[Key]>;
+
+export type GroupToParams<K extends string, G extends { [P in K]: unknown }> = {
+    [Key in K]?: GroupKeyToParam<K, G, Key>;
+};
\ No newline at end of file
diff --git a/projects/ngqp/core/src/lib/query-param-builder.service.ts b/projects/ngqp/core/src/lib/query-param-builder.service.ts
index 7322297..56e09cb 100644
--- a/projects/ngqp/core/src/lib/query-param-builder.service.ts
+++ b/projects/ngqp/core/src/lib/query-param-builder.service.ts
@@ -9,7 +9,7 @@ import {
 } from './serializers';
 import { LOOSE_IDENTITY_COMPARATOR } from './util';
 import { RouterOptions } from './router-adapter/router-adapter.interface';
-import { MultiQueryParam, QueryParam, PartitionedQueryParam } from './model/query-param';
+import { MultiQueryParam, QueryParam, PartitionedQueryParam, GroupToParams } from './model/query-param';
 import { QueryParamGroup } from './model/query-param-group';
 import { MultiQueryParamOpts, PartitionedQueryParamOpts, QueryParamOpts } from './model/query-param-opts';
 
@@ -38,10 +38,10 @@ export class QueryParamBuilder {
      * @param extras Additional parameters for this group, overriding global configuration.
      * @returns The new {@link QueryParamGroup}.
      */
-    public group(
-        queryParams: { [ name: string ]: QueryParam<unknown> | MultiQueryParam<unknown> | PartitionedQueryParam<unknown> },
+    public group<K extends string = string, G extends { [Key in K]: unknown } = any>(
+        queryParams: GroupToParams<K, G>,
         extras: RouterOptions = {}
-    ): QueryParamGroup {
+    ): QueryParamGroup<K, G> {
         // TODO Maybe we should first validate that no two queryParams defined the same "param".
         return new QueryParamGroup(queryParams, extras);
     }

@Airblader
Copy link
Collaborator Author

I'd say this is OK if type inference would work out of the box, but it doesn't seem to. I think this may need to be on the backburner for a while longer, perhaps TypeScript will catch up (or someone tells me how to do it properly ;-) )

@Airblader Airblader added Status: Discussion We're not yet sure whether or how to implement this feature and removed Status: Accepted This issue has been approved and PRs will be accepted labels Mar 30, 2019
@Airblader Airblader removed this from To do in @Airblader's Task Board Mar 30, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Comp: Core Core functionality of ngqp Priority: Low This is nice-to-have, but doesn't really affect users directly Status: Discussion We're not yet sure whether or how to implement this feature Type: Feature If you want to add something that doesn't exist yet
Projects
None yet
Development

No branches or pull requests

1 participant