@@ -56,6 +56,29 @@ export interface FeatureState {
5656/** Identifies a type of state an Action may have. */
5757export type StateFeature = keyof FeatureState ;
5858
59+ /**
60+ * The `Env` feature implies the availability of the `ReadOnlyEnv` feature.
61+ *
62+ * If `T` is `Env`, this returns `Env | ReadOnlyEnv`.
63+ * Otherwise, it is the identity and returns T.
64+ */
65+ type ImpliedFeatures < T extends StateFeature > = T extends "Env"
66+ ? "Env" | "ReadOnlyEnv"
67+ : T ;
68+
69+ /**
70+ * Given an object type `Obj`, this tries to lookup a corresponding `StateFeature`
71+ * to which the object type belongs in `FeatureState`. Resolves to `never` if there
72+ * is no match.
73+ */
74+ type FeatureNameFor < Obj extends object > = {
75+ [ K in StateFeature ] : [ Obj ] extends [ FeatureState [ K ] ]
76+ ? [ FeatureState [ K ] ] extends [ Obj ]
77+ ? K
78+ : never
79+ : never ;
80+ } [ StateFeature ] ;
81+
5982/** Constructs the intersection of all state types identifies by `Fs`. */
6083export type FieldsOf < Fs extends readonly StateFeature [ ] > = Fs extends [ ]
6184 ? Record < never , never >
@@ -66,8 +89,54 @@ export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
6689 ? FeatureState [ Head ] & FieldsOf < Tail >
6790 : never ;
6891
92+ /**
93+ * Symbol used for a field in `ActionState` that carries the type array of state features.
94+ * This is a Symbol so that it doesn't clash with any property names we might want to have.
95+ */
96+ const stateFeatures = Symbol ( ) ;
97+
6998/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
70- export type ActionState < Fs extends readonly StateFeature [ ] > = FieldsOf < Fs > ;
99+ export type ActionState < Fs extends readonly StateFeature [ ] > = FieldsOf < Fs > & {
100+ /**
101+ * When given a chance, TypeScript will simplify an `ActionState<Fs>` type as much as possible,
102+ * which results in a concrete object type that doesn't mention `Fs`.
103+ *
104+ * That causes problems for functions which accept `ActionState<Fs>` values, but need to know the
105+ * feature keys `Fs`. This property here explicitly captures `Fs` in the concrete object type
106+ * that results from simplifying `ActionState<Fs>`.
107+ *
108+ * This is a function rather than a field, because we want to be able to provide values of type
109+ * `ActionState<Fs>` to functions expecting `ActionState<As>` where `As` is a subset of `Fs`.
110+ *
111+ * Since function types are contravariant in the types of their parameters, using a function
112+ * type here allows that to happen.
113+ *
114+ * Because the field is optional, we don't have to explicitly provide a value
115+ * for it anywhere while the type is still inferred.
116+ *
117+ * `Fs[number]` returns the union of all features in `Fs`. We wrap it in `ImpliedFeatures`
118+ * so that `Env` is expanded into `Env | ReadOnlyEnv`, allowing functions that expect the
119+ * `ReadOnlyEnv` feature to be provided with an `ActionState` that has the `Env` feature
120+ * without requiring this to be made explicit.
121+ */
122+ readonly [ stateFeatures ] ?: ( ts : ImpliedFeatures < Fs [ number ] > ) => void ;
123+ } ;
124+
125+ /** Extends `state` with an `extra` feature. */
126+ export function extendActionState <
127+ // In first position, so that it can be explicitly provided if `FeatureNameFor`
128+ // should not work on `extra`.
129+ F extends StateFeature ,
130+ Fs extends readonly StateFeature [ ] ,
131+ E extends FeatureState [ F ] ,
132+ > (
133+ state : ActionState < Fs > ,
134+ extra : E ,
135+ ) : ActionState < [ ...Fs , FeatureNameFor < E > & F ] > {
136+ return { ...state , ...extra } as unknown as ActionState <
137+ [ ...Fs , FeatureNameFor < E > & F ]
138+ > ;
139+ }
71140
72141/** The type of an Action's main entry point. This is a function that is provided
73142 * with a basic `ActionState` object with features that are always available.
0 commit comments