diff --git a/lib/flagsmith/package.json b/lib/flagsmith/package.json index b4f6bed1..49b18dac 100644 --- a/lib/flagsmith/package.json +++ b/lib/flagsmith/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith", - "version": "9.0.5", + "version": "9.1.0", "description": "Feature flagging to support continuous development", "main": "./index.js", "module": "./index.mjs", diff --git a/lib/react-native-flagsmith/package.json b/lib/react-native-flagsmith/package.json index 74db1596..cd6bbffc 100644 --- a/lib/react-native-flagsmith/package.json +++ b/lib/react-native-flagsmith/package.json @@ -1,6 +1,6 @@ { "name": "react-native-flagsmith", - "version": "9.0.5", + "version": "9.1.0", "description": "Feature flagging to support continuous development", "main": "./index.js", "repository": { diff --git a/react.d.ts b/react.d.ts index c9d9bb70..e4f0cc97 100644 --- a/react.d.ts +++ b/react.d.ts @@ -8,11 +8,25 @@ export declare type FlagsmithContextType; -export declare function useFlags(_flags: readonly F[], _traits?: readonly T[]): { +type UseFlagsReturn< + F extends string | Record, + T extends string +> = F extends string + ? { [K in F]: IFlagsmithFeature; } & { [K in T]: IFlagsmithTrait; +} + : { + [K in keyof F]: IFlagsmithFeature; +} & { + [K in T]: IFlagsmithTrait; }; -export declare const useFlagsmith: () => IFlagsmith; +export declare const FlagsmithProvider: FC; +export declare function useFlags< + F extends string | Record, + T extends string = string +>(_flags: readonly F[], _traits?: readonly T[]): UseFlagsReturn; +export declare const useFlagsmith: , + T extends string = string>() => IFlagsmith; export declare const useFlagsmithLoading: () => LoadingState | undefined; diff --git a/react.tsx b/react.tsx index 356e8fb4..df1d5379 100644 --- a/react.tsx +++ b/react.tsx @@ -126,11 +126,40 @@ export function useFlagsmithLoading() { return loadingState } -export function useFlags(_flags: readonly F[], _traits: readonly T[] = []): { - [K in F]: IFlagsmithFeature +type UseFlagsReturn< + F extends string | Record, + T extends string +> = F extends string + ? { + [K in F]: IFlagsmithFeature; } & { - [K in T]: IFlagsmithTrait -} { + [K in T]: IFlagsmithTrait; +} + : { + [K in keyof F]: IFlagsmithFeature; +} & { + [K in T]: IFlagsmithTrait; +}; + +/** + * Example usage: + * + * // A) Using string flags: + * useFlags<"featureOne"|"featureTwo">(["featureOne", "featureTwo"]); + * + * // B) Using an object for F - this can be generated by our CLI: https://github.com/Flagsmith/flagsmith-cli : + * interface MyFeatureInterface { + * featureOne: string; + * featureTwo: number; + * } + * useFlags(["featureOne", "featureTwo"]); + */ +export function useFlags< + F extends string | Record, + T extends string = string +>( + _flags: readonly F[], _traits: readonly T[] = [] +){ const firstRender = useRef(true) const flags = useConstant(flagsAsArray(_flags)) const traits = useConstant(flagsAsArray(_traits)) @@ -173,15 +202,18 @@ export function useFlags(_flag return res }, [renderRef]) - return res + return res as UseFlagsReturn } -export function useFlagsmith() { +export function useFlagsmith< + F extends string | Record, + T extends string = string +>() { const context = useContext(FlagsmithContext) if (!context) { throw new Error('useFlagsmith must be used with in a FlagsmithProvider') } - return context as IFlagsmith + return context as unknown as IFlagsmith } diff --git a/test/default-flags.test.ts b/test/default-flags.test.ts index a95548e3..75896d09 100644 --- a/test/default-flags.test.ts +++ b/test/default-flags.test.ts @@ -1,4 +1,3 @@ -// Sample test import { defaultState, defaultStateAlt, FLAGSMITH_KEY, getFlagsmith, getStateToCheck } from './test-constants'; import { IFlags } from '../types'; diff --git a/test/functions.test.ts b/test/functions.test.ts index 2c5d41ae..e84e2ac0 100644 --- a/test/functions.test.ts +++ b/test/functions.test.ts @@ -1,4 +1,3 @@ -// Sample test import { getFlagsmith } from './test-constants'; describe('Flagsmith.functions', () => { diff --git a/test/init.test.ts b/test/init.test.ts index 3055f00d..91eff2d2 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -1,4 +1,3 @@ -// Sample test import { waitFor } from '@testing-library/react'; import {defaultState, FLAGSMITH_KEY, getFlagsmith, getStateToCheck, identityState} from './test-constants'; import { promises as fs } from 'fs'; diff --git a/test/react-types.test.tsx b/test/react-types.test.tsx new file mode 100644 index 00000000..c4256dd4 --- /dev/null +++ b/test/react-types.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import {FlagsmithProvider, useFlags, useFlagsmith} from '../lib/flagsmith/react'; +import {getFlagsmith,} from './test-constants'; + + +describe.only('FlagsmithProvider', () => { + it('should allow supplying interface generics to useFlagsmith', () => { + const FlagsmithPage = ()=> { + const typedFlagsmith = useFlagsmith< + { + stringFlag: string + numberFlag: number + objectFlag: { first_name: string } + } + >() + //@ts-expect-error - feature not defined + typedFlagsmith.hasFeature("fail") + //@ts-expect-error - feature not defined + typedFlagsmith.getValue("fail") + + typedFlagsmith.hasFeature("stringFlag") + typedFlagsmith.hasFeature("numberFlag") + typedFlagsmith.getValue("stringFlag") + typedFlagsmith.getValue("numberFlag") + + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const stringFlag: string|null = typedFlagsmith.getValue("stringFlag") + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const numberFlag: number|null = typedFlagsmith.getValue("numberFlag") + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const firstName: string | undefined = typedFlagsmith.getValue("objectFlag")?.first_name + + // @ts-expect-error - invalid does not exist on type announcement + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const invalidPointer: string | undefined = typedFlagsmith.getValue("objectFlag")?.invalid + + // @ts-expect-error - feature should be a number + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const incorrectNumberFlag: string = typedFlagsmith.getValue("numberFlag") + + return <> + } + const onChange = jest.fn(); + const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange}) + render( + + + + ); + }); + it('should allow supplying interface generics to useFlags', () => { + const FlagsmithPage = ()=> { + const typedFlagsmith = useFlags< + { + stringFlag: string + numberFlag: number + objectFlag: { first_name: string } + } + >([]) + //@ts-expect-error - feature not defined + typedFlagsmith.fail?.enabled + //@ts-expect-error - feature not defined + typedFlagsmith.fail?.value + + typedFlagsmith.numberFlag + typedFlagsmith.stringFlag + typedFlagsmith.objectFlag + + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const stringFlag: string = typedFlagsmith.stringFlag?.value + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const numberFlag: number = typedFlagsmith.numberFlag?.value + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const firstName: string = typedFlagsmith.objectFlag?.value.first_name + + // @ts-expect-error - invalid does not exist on type announcement + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const invalidPointer: string = typedFlagsmith.objectFlag?.value.invalid + + // @ts-expect-error - feature should be a number + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const incorrectNumberFlag: string = typedFlagsmith.numberFlag?.value + + return <> + } + const onChange = jest.fn(); + const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange}) + render( + + + + ); + }); +}); diff --git a/test/types.test.ts b/test/types.test.ts new file mode 100644 index 00000000..7c84dc2a --- /dev/null +++ b/test/types.test.ts @@ -0,0 +1,55 @@ +// Sample test +import {getFlagsmith} from './test-constants'; +import {IFlagsmith} from '../types'; + +describe('Flagsmith Types', () => { + + // The following tests will fail to compile if any of the types fail / expect-error has no type issues + // Therefor all of the following ts-expect-errors and eslint-disable-lines are by design + test('should allow supplying string generics to a flagsmith instance', async () => { + const { flagsmith, } = getFlagsmith({ }); + const typedFlagsmith = flagsmith as IFlagsmith<"flag1"|"flag2"> + //@ts-expect-error - feature not defined + typedFlagsmith.hasFeature("fail") + //@ts-expect-error - feature not defined + typedFlagsmith.getValue("fail") + + typedFlagsmith.hasFeature("flag1") + typedFlagsmith.hasFeature("flag2") + typedFlagsmith.getValue("flag1") + typedFlagsmith.getValue("flag2") + }); + test('should allow supplying interface generics to a flagsmith instance', async () => { + const { flagsmith, } = getFlagsmith({ }); + const typedFlagsmith = flagsmith as IFlagsmith< + { + stringFlag: string + numberFlag: number + objectFlag: { first_name: string } + }> + //@ts-expect-error - feature not defined + typedFlagsmith.hasFeature("fail") + //@ts-expect-error - feature not defined + typedFlagsmith.getValue("fail") + + typedFlagsmith.hasFeature("stringFlag") + typedFlagsmith.hasFeature("numberFlag") + typedFlagsmith.getValue("stringFlag") + typedFlagsmith.getValue("numberFlag") + + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const stringFlag: string | null = typedFlagsmith.getValue("stringFlag") + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const numberFlag: number | null = typedFlagsmith.getValue("numberFlag") + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const firstName: string | undefined = typedFlagsmith.getValue("objectFlag")?.first_name + + // @ts-expect-error - invalid does not exist on type announcement + //eslint-disable-next-line @typescript-eslint/no-unused-vars + const invalidPointer: string = typedFlagsmith.getValue("objectFlag")?.invalid + + // @ts-expect-error - feature should be a number + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const incorrectNumberFlag: string = typedFlagsmith.getValue("numberFlag") + }); +}); diff --git a/types.d.ts b/types.d.ts index b347ed43..f39908f6 100644 --- a/types.d.ts +++ b/types.d.ts @@ -8,10 +8,11 @@ export type DynatraceObject = { "shortString": Record, "javaDouble": Record, } -export interface IFlagsmithFeature { - id?: number; + +export interface IFlagsmithFeature { + id: numbers enabled: boolean; - value?: IFlagsmithValue; + value: Value; } export declare type IFlagsmithTrait = IFlagsmithValue | TraitEvaluationContext; @@ -132,8 +133,28 @@ export interface IFlagsmithResponse { }; }[]; } - -export interface IFlagsmith { +type FKey = F extends string ? F : keyof F; +type FValue> = F extends string + ? IFlagsmithValue + : F[K] | null; +/** + * Example usage: + * + * // A) Using string flags: + * import flagsmith from 'flagsmith' as IFlagsmith<"featureOne"|"featureTwo">; + * + * // B) Using an object for F - this can be generated by our CLI: https://github.com/Flagsmith/flagsmith-cli : + * interface MyFeatureInterface { + * featureOne: string; + * featureTwo: number; + * } + * import flagsmith from 'flagsmith' as IFlagsmith; + */ +export interface IFlagsmith< +F extends string | Record = string, +T extends string = string +> +{ /** * Initialise the sdk against a particular environment */ @@ -194,7 +215,7 @@ export interface IFlagsmith boolean; + hasFeature: (key: FKey, optionsOrSkipAnalytics?: HasFeatureOptions) => boolean; /** * Returns the value of a feature, or a fallback value. @@ -212,8 +233,11 @@ export interface IFlagsmith(key: F, options?: GetValueOptions, skipAnalytics?: boolean): IFlagsmithValue; - + getValue>( + key: K, + options?: GetValueOptions>, + skipAnalytics?: boolean + ): IFlagsmithValue>; /** * Get the value of a particular trait for the identified user */