Skip to content

Commit

Permalink
feat!: improve generic type accuracy (#224)
Browse files Browse the repository at this point in the history
- 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]>
  • Loading branch information
toddbaert authored Sep 20, 2022
1 parent a15c41a commit 12230a5
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 76 deletions.
71 changes: 40 additions & 31 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
FlagValueType,
Hook,
HookContext,
JsonValue,
Logger,
Provider,
ResolutionDetails,
Expand Down Expand Up @@ -154,38 +155,41 @@ export class OpenFeatureClient implements Client {
* Performs a flag evaluation that returns a string.
*
* @param {string} flagKey The flag key uniquely identifies a particular flag
* @param {string} defaultValue The value returned if an error occurs
* @template {string} T A optional generic argument constraining the string
* @param {T} defaultValue The value returned if an error occurs
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<string>} Flag evaluation response
* @returns {Promise<T>} Flag evaluation response
*/
async getStringValue(
async getStringValue<T extends string = string>(
flagKey: string,
defaultValue: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<string> {
return (await this.getStringDetails(flagKey, defaultValue, context, options)).value;
): Promise<T> {
return (await this.getStringDetails<T>(flagKey, defaultValue, context, options)).value;
}

/**
* Performs a flag evaluation that a returns an evaluation details object.
*
* @param {string} flagKey The flag key uniquely identifies a particular flag
* @param {boolean} defaultValue The value returned if an error occurs
* @template {string} T A optional generic argument constraining the string
* @param {T} defaultValue The value returned if an error occurs
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<EvaluationDetails<string>>} Flag evaluation details response
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
*/
getStringDetails(
getStringDetails<T extends string = string>(
flagKey: string,
defaultValue: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<string>> {
return this.evaluate<string>(
): Promise<EvaluationDetails<T>> {
return this.evaluate<T>(
flagKey,
this._provider.resolveStringEvaluation,
// this isolates providers from our restricted string generic argument.
this._provider.resolveStringEvaluation as () => Promise<EvaluationDetails<T>>,
defaultValue,
'string',
context,
Expand All @@ -197,38 +201,41 @@ export class OpenFeatureClient implements Client {
* Performs a flag evaluation that returns a number.
*
* @param {string} flagKey The flag key uniquely identifies a particular flag
* @param {number} defaultValue The value returned if an error occurs
* @template {number} T A optional generic argument constraining the number
* @param {T} defaultValue The value returned if an error occurs
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<number>} Flag evaluation response
* @returns {Promise<T>} Flag evaluation response
*/
async getNumberValue(
async getNumberValue<T extends number = number>(
flagKey: string,
defaultValue: number,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<number> {
): Promise<T> {
return (await this.getNumberDetails(flagKey, defaultValue, context, options)).value;
}

/**
* Performs a flag evaluation that a returns an evaluation details object.
*
* @param {string} flagKey The flag key uniquely identifies a particular flag
* @param {number} defaultValue The value returned if an error occurs
* @template {number} T A optional generic argument constraining the number
* @param {T} defaultValue The value returned if an error occurs
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<EvaluationDetails<number>>} Flag evaluation details response
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
*/
getNumberDetails(
getNumberDetails<T extends number = number>(
flagKey: string,
defaultValue: number,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<number>> {
return this.evaluate<number>(
): Promise<EvaluationDetails<T>> {
return this.evaluate<T>(
flagKey,
this._provider.resolveNumberEvaluation,
// this isolates providers from our restricted number generic argument.
this._provider.resolveNumberEvaluation as () => Promise<EvaluationDetails<T>>,
defaultValue,
'number',
context,
Expand All @@ -240,12 +247,13 @@ export class OpenFeatureClient implements Client {
* Performs a flag evaluation that returns an object.
*
* @param {string} flagKey The flag key uniquely identifies a particular flag
* @param {object} defaultValue The value returned if an error occurs
* @template {JsonValue} T A optional generic argument describing the structure
* @param {T} defaultValue The value returned if an error occurs
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<object>} Flag evaluation response
* @returns {Promise<T>} Flag evaluation response
*/
async getObjectValue<T extends object>(
async getObjectValue<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
Expand All @@ -258,12 +266,13 @@ export class OpenFeatureClient implements Client {
* Performs a flag evaluation that a returns an evaluation details object.
*
* @param {string} flagKey The flag key uniquely identifies a particular flag
* @param {object} defaultValue The value returned if an error occurs
* @template {JsonValue} T A optional generic argument describing the structure
* @param {T} defaultValue The value returned if an error occurs
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<EvaluationDetails<object>>} Flag evaluation details response
* @returns {Promise<EvaluationDetails<T>>} Flag evaluation details response
*/
getObjectDetails<T extends object>(
getObjectDetails<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
Expand Down
4 changes: 2 additions & 2 deletions src/no-op-provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Provider, ResolutionDetails } from './types';
import { JsonValue, Provider, ResolutionDetails } from './types';

const REASON_NO_OP = 'No-op';

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

resolveObjectEvaluation<T extends object>(_: string, defaultValue: T): Promise<ResolutionDetails<T>> {
resolveObjectEvaluation<T extends JsonValue>(_: string, defaultValue: T): Promise<ResolutionDetails<T>> {
return this.noOp<T>(defaultValue);
}

Expand Down
75 changes: 63 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
type PrimitiveValue = null | boolean | string | number ;

export type JsonObject = { [key: string]: JsonValue };

export type JsonArray = JsonValue[];

/**
* Represents a JSON value of a JSON object
* Represents a JSON node value.
*/
export type JSONValue = null | string | number | boolean | Date | { [x: string]: JSONValue } | Array<JSONValue>;
export type JsonValue = PrimitiveValue | JsonObject | JsonArray;

/**
* Represents a JSON node value, or Date.
*/
export type EvaluationContextValue = PrimitiveValue | Date | { [key: string]: EvaluationContextValue } | EvaluationContextValue[];

/**
* A container for arbitrary contextual data that can be used as a basis for dynamic evaluation
*/
export type EvaluationContext = {
/**
* A string uniquely identifying the subject (end-user, or client service) of a flag evaluation.
* 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.
* 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.
*/
targetingKey?: string;
} & Record<string, JSONValue>;
} & Record<string, EvaluationContextValue>;

export type FlagValue = boolean | string | number | object;
export type FlagValue = boolean | string | number | JsonValue;

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

Expand Down Expand Up @@ -51,12 +66,18 @@ export interface Features {
/**
* Get a string flag value.
*/
getStringValue(
getStringValue(
flagKey: string,
defaultValue: string,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<string>;
getStringValue<T extends string = string>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<T>;

/**
* Get a string flag with additional details.
Expand All @@ -67,6 +88,12 @@ export interface Features {
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<string>>;
getStringDetails<T extends string = string>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<T>>;

/**
* Get a number flag value.
Expand All @@ -77,21 +104,39 @@ export interface Features {
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<number>;
getNumberValue<T extends number = number>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<T>;

/**
* Get a number flag with additional details.
*/
getNumberDetails(
getNumberDetails(
flagKey: string,
defaultValue: number,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<number>>;
getNumberDetails<T extends number = number>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<T>>;

/**
* Get an object (JSON) flag value.
*/
getObjectValue<T extends object>(
getObjectValue(
flagKey: string,
defaultValue: JsonValue,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<JsonValue>;
getObjectValue<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
Expand All @@ -101,7 +146,13 @@ export interface Features {
/**
* Get an object (JSON) flag with additional details.
*/
getObjectDetails<T extends object>(
getObjectDetails(
flagKey: string,
defaultValue: JsonValue,
context?: EvaluationContext,
options?: FlagEvaluationOptions
): Promise<EvaluationDetails<JsonValue>>;
getObjectDetails<T extends JsonValue = JsonValue>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
Expand Down Expand Up @@ -158,12 +209,12 @@ export interface Provider {
/**
* Resolve and parse an object flag and its evaluation details.
*/
resolveObjectEvaluation<U extends object>(
resolveObjectEvaluation<T extends JsonValue>(
flagKey: string,
defaultValue: U,
defaultValue: T,
context: EvaluationContext,
logger: Logger
): Promise<ResolutionDetails<U>>;
): Promise<ResolutionDetails<T>>;
}

export enum StandardResolutionReasons {
Expand Down
Loading

0 comments on commit 12230a5

Please sign in to comment.