Skip to content

Commit 12230a5

Browse files
authored
feat!: improve generic type accuracy (#224)
- restricts generic args on object resolver (`T` is now `T extends JsonValue`) - adds optional generic args for string/number resolver for unions of string/number literals Signed-off-by: Todd Baert <[email protected]>
1 parent a15c41a commit 12230a5

File tree

5 files changed

+221
-76
lines changed

5 files changed

+221
-76
lines changed

src/client.ts

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
FlagValueType,
1212
Hook,
1313
HookContext,
14+
JsonValue,
1415
Logger,
1516
Provider,
1617
ResolutionDetails,
@@ -154,38 +155,41 @@ export class OpenFeatureClient implements Client {
154155
* Performs a flag evaluation that returns a string.
155156
*
156157
* @param {string} flagKey The flag key uniquely identifies a particular flag
157-
* @param {string} defaultValue The value returned if an error occurs
158+
* @template {string} T A optional generic argument constraining the string
159+
* @param {T} defaultValue The value returned if an error occurs
158160
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
159161
* @param {FlagEvaluationOptions} options Additional flag evaluation options
160-
* @returns {Promise<string>} Flag evaluation response
162+
* @returns {Promise<T>} Flag evaluation response
161163
*/
162-
async getStringValue(
164+
async getStringValue<T extends string = string>(
163165
flagKey: string,
164-
defaultValue: string,
166+
defaultValue: T,
165167
context?: EvaluationContext,
166168
options?: FlagEvaluationOptions
167-
): Promise<string> {
168-
return (await this.getStringDetails(flagKey, defaultValue, context, options)).value;
169+
): Promise<T> {
170+
return (await this.getStringDetails<T>(flagKey, defaultValue, context, options)).value;
169171
}
170172

171173
/**
172174
* Performs a flag evaluation that a returns an evaluation details object.
173175
*
174176
* @param {string} flagKey The flag key uniquely identifies a particular flag
175-
* @param {boolean} defaultValue The value returned if an error occurs
177+
* @template {string} T A optional generic argument constraining the string
178+
* @param {T} defaultValue The value returned if an error occurs
176179
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
177180
* @param {FlagEvaluationOptions} options Additional flag evaluation options
178-
* @returns {Promise<EvaluationDetails<string>>} Flag evaluation details response
181+
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
179182
*/
180-
getStringDetails(
183+
getStringDetails<T extends string = string>(
181184
flagKey: string,
182-
defaultValue: string,
185+
defaultValue: T,
183186
context?: EvaluationContext,
184187
options?: FlagEvaluationOptions
185-
): Promise<EvaluationDetails<string>> {
186-
return this.evaluate<string>(
188+
): Promise<EvaluationDetails<T>> {
189+
return this.evaluate<T>(
187190
flagKey,
188-
this._provider.resolveStringEvaluation,
191+
// this isolates providers from our restricted string generic argument.
192+
this._provider.resolveStringEvaluation as () => Promise<EvaluationDetails<T>>,
189193
defaultValue,
190194
'string',
191195
context,
@@ -197,38 +201,41 @@ export class OpenFeatureClient implements Client {
197201
* Performs a flag evaluation that returns a number.
198202
*
199203
* @param {string} flagKey The flag key uniquely identifies a particular flag
200-
* @param {number} defaultValue The value returned if an error occurs
204+
* @template {number} T A optional generic argument constraining the number
205+
* @param {T} defaultValue The value returned if an error occurs
201206
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
202207
* @param {FlagEvaluationOptions} options Additional flag evaluation options
203-
* @returns {Promise<number>} Flag evaluation response
208+
* @returns {Promise<T>} Flag evaluation response
204209
*/
205-
async getNumberValue(
210+
async getNumberValue<T extends number = number>(
206211
flagKey: string,
207-
defaultValue: number,
212+
defaultValue: T,
208213
context?: EvaluationContext,
209214
options?: FlagEvaluationOptions
210-
): Promise<number> {
215+
): Promise<T> {
211216
return (await this.getNumberDetails(flagKey, defaultValue, context, options)).value;
212217
}
213218

214219
/**
215220
* Performs a flag evaluation that a returns an evaluation details object.
216221
*
217222
* @param {string} flagKey The flag key uniquely identifies a particular flag
218-
* @param {number} defaultValue The value returned if an error occurs
223+
* @template {number} T A optional generic argument constraining the number
224+
* @param {T} defaultValue The value returned if an error occurs
219225
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
220226
* @param {FlagEvaluationOptions} options Additional flag evaluation options
221-
* @returns {Promise<EvaluationDetails<number>>} Flag evaluation details response
227+
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
222228
*/
223-
getNumberDetails(
229+
getNumberDetails<T extends number = number>(
224230
flagKey: string,
225-
defaultValue: number,
231+
defaultValue: T,
226232
context?: EvaluationContext,
227233
options?: FlagEvaluationOptions
228-
): Promise<EvaluationDetails<number>> {
229-
return this.evaluate<number>(
234+
): Promise<EvaluationDetails<T>> {
235+
return this.evaluate<T>(
230236
flagKey,
231-
this._provider.resolveNumberEvaluation,
237+
// this isolates providers from our restricted number generic argument.
238+
this._provider.resolveNumberEvaluation as () => Promise<EvaluationDetails<T>>,
232239
defaultValue,
233240
'number',
234241
context,
@@ -240,12 +247,13 @@ export class OpenFeatureClient implements Client {
240247
* Performs a flag evaluation that returns an object.
241248
*
242249
* @param {string} flagKey The flag key uniquely identifies a particular flag
243-
* @param {object} defaultValue The value returned if an error occurs
250+
* @template {JsonValue} T A optional generic argument describing the structure
251+
* @param {T} defaultValue The value returned if an error occurs
244252
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
245253
* @param {FlagEvaluationOptions} options Additional flag evaluation options
246-
* @returns {Promise<object>} Flag evaluation response
254+
* @returns {Promise<T>} Flag evaluation response
247255
*/
248-
async getObjectValue<T extends object>(
256+
async getObjectValue<T extends JsonValue = JsonValue>(
249257
flagKey: string,
250258
defaultValue: T,
251259
context?: EvaluationContext,
@@ -258,12 +266,13 @@ export class OpenFeatureClient implements Client {
258266
* Performs a flag evaluation that a returns an evaluation details object.
259267
*
260268
* @param {string} flagKey The flag key uniquely identifies a particular flag
261-
* @param {object} defaultValue The value returned if an error occurs
269+
* @template {JsonValue} T A optional generic argument describing the structure
270+
* @param {T} defaultValue The value returned if an error occurs
262271
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
263272
* @param {FlagEvaluationOptions} options Additional flag evaluation options
264-
* @returns {Promise<EvaluationDetails<object>>} Flag evaluation details response
273+
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
265274
*/
266-
getObjectDetails<T extends object>(
275+
getObjectDetails<T extends JsonValue = JsonValue>(
267276
flagKey: string,
268277
defaultValue: T,
269278
context?: EvaluationContext,

src/no-op-provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Provider, ResolutionDetails } from './types';
1+
import { JsonValue, Provider, ResolutionDetails } from './types';
22

33
const REASON_NO_OP = 'No-op';
44

@@ -22,7 +22,7 @@ class NoopFeatureProvider implements Provider {
2222
return this.noOp(defaultValue);
2323
}
2424

25-
resolveObjectEvaluation<T extends object>(_: string, defaultValue: T): Promise<ResolutionDetails<T>> {
25+
resolveObjectEvaluation<T extends JsonValue>(_: string, defaultValue: T): Promise<ResolutionDetails<T>> {
2626
return this.noOp<T>(defaultValue);
2727
}
2828

src/types.ts

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1+
type PrimitiveValue = null | boolean | string | number ;
2+
3+
export type JsonObject = { [key: string]: JsonValue };
4+
5+
export type JsonArray = JsonValue[];
6+
17
/**
2-
* Represents a JSON value of a JSON object
8+
* Represents a JSON node value.
39
*/
4-
export type JSONValue = null | string | number | boolean | Date | { [x: string]: JSONValue } | Array<JSONValue>;
10+
export type JsonValue = PrimitiveValue | JsonObject | JsonArray;
511

12+
/**
13+
* Represents a JSON node value, or Date.
14+
*/
15+
export type EvaluationContextValue = PrimitiveValue | Date | { [key: string]: EvaluationContextValue } | EvaluationContextValue[];
16+
17+
/**
18+
* A container for arbitrary contextual data that can be used as a basis for dynamic evaluation
19+
*/
620
export type EvaluationContext = {
721
/**
822
* A string uniquely identifying the subject (end-user, or client service) of a flag evaluation.
9-
* Providers may require this field for fractional flag evaluation, rules, or overrides targeting specific users. Such providers may behave unpredictably if a targeting key is not specified at flag resolution.
23+
* Providers may require this field for fractional flag evaluation, rules, or overrides targeting specific users.
24+
* Such providers may behave unpredictably if a targeting key is not specified at flag resolution.
1025
*/
1126
targetingKey?: string;
12-
} & Record<string, JSONValue>;
27+
} & Record<string, EvaluationContextValue>;
1328

14-
export type FlagValue = boolean | string | number | object;
29+
export type FlagValue = boolean | string | number | JsonValue;
1530

1631
export type FlagValueType = 'boolean' | 'string' | 'number' | 'object';
1732

@@ -51,12 +66,18 @@ export interface Features {
5166
/**
5267
* Get a string flag value.
5368
*/
54-
getStringValue(
69+
getStringValue(
5570
flagKey: string,
5671
defaultValue: string,
5772
context?: EvaluationContext,
5873
options?: FlagEvaluationOptions
5974
): Promise<string>;
75+
getStringValue<T extends string = string>(
76+
flagKey: string,
77+
defaultValue: T,
78+
context?: EvaluationContext,
79+
options?: FlagEvaluationOptions
80+
): Promise<T>;
6081

6182
/**
6283
* Get a string flag with additional details.
@@ -67,6 +88,12 @@ export interface Features {
6788
context?: EvaluationContext,
6889
options?: FlagEvaluationOptions
6990
): Promise<EvaluationDetails<string>>;
91+
getStringDetails<T extends string = string>(
92+
flagKey: string,
93+
defaultValue: T,
94+
context?: EvaluationContext,
95+
options?: FlagEvaluationOptions
96+
): Promise<EvaluationDetails<T>>;
7097

7198
/**
7299
* Get a number flag value.
@@ -77,21 +104,39 @@ export interface Features {
77104
context?: EvaluationContext,
78105
options?: FlagEvaluationOptions
79106
): Promise<number>;
107+
getNumberValue<T extends number = number>(
108+
flagKey: string,
109+
defaultValue: T,
110+
context?: EvaluationContext,
111+
options?: FlagEvaluationOptions
112+
): Promise<T>;
80113

81114
/**
82115
* Get a number flag with additional details.
83116
*/
84-
getNumberDetails(
117+
getNumberDetails(
85118
flagKey: string,
86119
defaultValue: number,
87120
context?: EvaluationContext,
88121
options?: FlagEvaluationOptions
89122
): Promise<EvaluationDetails<number>>;
123+
getNumberDetails<T extends number = number>(
124+
flagKey: string,
125+
defaultValue: T,
126+
context?: EvaluationContext,
127+
options?: FlagEvaluationOptions
128+
): Promise<EvaluationDetails<T>>;
90129

91130
/**
92131
* Get an object (JSON) flag value.
93132
*/
94-
getObjectValue<T extends object>(
133+
getObjectValue(
134+
flagKey: string,
135+
defaultValue: JsonValue,
136+
context?: EvaluationContext,
137+
options?: FlagEvaluationOptions
138+
): Promise<JsonValue>;
139+
getObjectValue<T extends JsonValue = JsonValue>(
95140
flagKey: string,
96141
defaultValue: T,
97142
context?: EvaluationContext,
@@ -101,7 +146,13 @@ export interface Features {
101146
/**
102147
* Get an object (JSON) flag with additional details.
103148
*/
104-
getObjectDetails<T extends object>(
149+
getObjectDetails(
150+
flagKey: string,
151+
defaultValue: JsonValue,
152+
context?: EvaluationContext,
153+
options?: FlagEvaluationOptions
154+
): Promise<EvaluationDetails<JsonValue>>;
155+
getObjectDetails<T extends JsonValue = JsonValue>(
105156
flagKey: string,
106157
defaultValue: T,
107158
context?: EvaluationContext,
@@ -158,12 +209,12 @@ export interface Provider {
158209
/**
159210
* Resolve and parse an object flag and its evaluation details.
160211
*/
161-
resolveObjectEvaluation<U extends object>(
212+
resolveObjectEvaluation<T extends JsonValue>(
162213
flagKey: string,
163-
defaultValue: U,
214+
defaultValue: T,
164215
context: EvaluationContext,
165216
logger: Logger
166-
): Promise<ResolutionDetails<U>>;
217+
): Promise<ResolutionDetails<T>>;
167218
}
168219

169220
export enum StandardResolutionReasons {

0 commit comments

Comments
 (0)