From 6300214de0cb1b210f7967d3f5a877d27e61c8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Oliva?= Date: Wed, 19 Oct 2022 15:16:36 +0200 Subject: [PATCH] feat: add compatible keys restriction to combineStates --- .../context-state/src/combineStates.test.ts | 10 +++-- packages/context-state/src/combineStates.ts | 40 ++++++++++++++++++- packages/context-state/src/route-state.ts | 8 ++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/context-state/src/combineStates.test.ts b/packages/context-state/src/combineStates.test.ts index e12a7c9..3edd287 100644 --- a/packages/context-state/src/combineStates.test.ts +++ b/packages/context-state/src/combineStates.test.ts @@ -2,7 +2,7 @@ import { createRoot } from "./create-root" import { BehaviorSubject, of, Subject } from "rxjs" import { substate } from "./substate" import { combineStates } from "./combineStates" -import { StateNode } from "./types" +import { StateNode, StringRecord } from "./types" import { routeState } from "./route-state" describe("combineStates", () => { @@ -43,7 +43,9 @@ describe("combineStates", () => { }) it("only activates if all branches are active", () => { - function createActivableNode(root: StateNode) { + function createActivableNode>( + root: StateNode, + ) { const ctxSource = new BehaviorSubject(false) const ctxNode = substate(root, () => ctxSource) const [, { node }] = routeState( @@ -84,7 +86,9 @@ describe("combineStates", () => { }) it("doesn't emit a value until all branches have one", async () => { - function createSettableNode(root: StateNode) { + function createSettableNode>( + root: StateNode, + ) { const source = new Subject() const node = substate(root, () => source) return [node, (value: any) => source.next(value)] as const diff --git a/packages/context-state/src/combineStates.ts b/packages/context-state/src/combineStates.ts index c74877d..4b8c618 100644 --- a/packages/context-state/src/combineStates.ts +++ b/packages/context-state/src/combineStates.ts @@ -28,7 +28,7 @@ interface CombinedStateInstance { } export const combineStates = >>( - states: States, + states: KeysAreCompatible> extends true ? States : never, ): StringRecordNodeToNodeStringRecord => { const instances = new NestedMap() const nKeys = Object.keys(states).length @@ -86,3 +86,41 @@ export const combineStates = >>( return result.public as StringRecordNodeToNodeStringRecord } + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never + +/** + * Converts Record> to Record + */ +type MapKeys = { + [K in keyof States]: States[K] extends StateNode ? K : never +} + +/** + * For each of the keys, check if they are compatible with the intersection + */ +type IndividualIsCompatible = { + [K in keyof KeysRecord]: KeysRecord[K] extends KeysIntersection ? true : false +} + +/** + * It will be compatible if one of the individual ones returns true. + * If all of them are false, true extends false => false + * if one of them is true, true extends boolean => true + */ +type IsCompatible = + true extends IndividualIsCompatible< + KeysRecord, + KeysIntersection + >[keyof KeysRecord] + ? true + : false + +type KeysAreCompatible = IsCompatible< + KeysRecord, + UnionToIntersection +> diff --git a/packages/context-state/src/route-state.ts b/packages/context-state/src/route-state.ts index 6adb2f7..17faa4b 100644 --- a/packages/context-state/src/route-state.ts +++ b/packages/context-state/src/route-state.ts @@ -24,10 +24,10 @@ export const routeState = < K extends StringRecord, O extends StringRecord<((value: T) => any) | null>, OT extends { - [K in keyof O]: null extends O[K] - ? StateNode - : O[K] extends (value: T) => infer V - ? StateNode + [KOT in keyof O]: null extends O[KOT] + ? StateNode + : O[KOT] extends (value: T) => infer V + ? StateNode : unknown }, >(