Skip to content

feat: Add interface types #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/flagsmith/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion lib/react-native-flagsmith/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
20 changes: 17 additions & 3 deletions react.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ export declare type FlagsmithContextType<F extends string = string, T extends st
serverState?: IState;
children: React.ReactElement[] | React.ReactElement;
};
export declare const FlagsmithProvider: FC<FlagsmithContextType>;
export declare function useFlags<F extends string, T extends string>(_flags: readonly F[], _traits?: readonly T[]): {
type UseFlagsReturn<
F extends string | Record<string, any>,
T extends string
> = F extends string
? {
[K in F]: IFlagsmithFeature;
} & {
[K in T]: IFlagsmithTrait;
}
: {
[K in keyof F]: IFlagsmithFeature<F[K]>;
} & {
[K in T]: IFlagsmithTrait;
};
export declare const useFlagsmith: () => IFlagsmith;
export declare const FlagsmithProvider: FC<FlagsmithContextType>;
export declare function useFlags<
F extends string | Record<string, any>,
T extends string = string
>(_flags: readonly F[], _traits?: readonly T[]): UseFlagsReturn<F, T>;
export declare const useFlagsmith: <F extends string | Record<string, any>,
T extends string = string>() => IFlagsmith<F, T>;
export declare const useFlagsmithLoading: () => LoadingState | undefined;
46 changes: 39 additions & 7 deletions react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,40 @@ export function useFlagsmithLoading() {
return loadingState
}

export function useFlags<F extends string=string, T extends string=string>(_flags: readonly F[], _traits: readonly T[] = []): {
[K in F]: IFlagsmithFeature
type UseFlagsReturn<
F extends string | Record<string, any>,
T extends string
> = F extends string
? {
[K in F]: IFlagsmithFeature;
} & {
[K in T]: IFlagsmithTrait
} {
[K in T]: IFlagsmithTrait;
}
: {
[K in keyof F]: IFlagsmithFeature<F[K]>;
} & {
[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<MyFeatureInterface>(["featureOne", "featureTwo"]);
*/
export function useFlags<
F extends string | Record<string, any>,
T extends string = string
>(
_flags: readonly F[], _traits: readonly T[] = []
){
const firstRender = useRef(true)
const flags = useConstant<string[]>(flagsAsArray(_flags))
const traits = useConstant<string[]>(flagsAsArray(_traits))
Expand Down Expand Up @@ -173,15 +202,18 @@ export function useFlags<F extends string=string, T extends string=string>(_flag
return res
}, [renderRef])

return res
return res as UseFlagsReturn<F, T>
}

export function useFlagsmith<F extends string=string, T extends string=string>() {
export function useFlagsmith<
F extends string | Record<string, any>,
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<F, T>
return context as unknown as IFlagsmith<F, T>
}
1 change: 0 additions & 1 deletion test/default-flags.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Sample test
import { defaultState, defaultStateAlt, FLAGSMITH_KEY, getFlagsmith, getStateToCheck } from './test-constants';
import { IFlags } from '../types';

Expand Down
1 change: 0 additions & 1 deletion test/functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Sample test
import { getFlagsmith } from './test-constants';

describe('Flagsmith.functions', () => {
Expand Down
1 change: 0 additions & 1 deletion test/init.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
95 changes: 95 additions & 0 deletions test/react-types.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<FlagsmithProvider flagsmith={flagsmith} options={initConfig}>
<FlagsmithPage/>
</FlagsmithProvider>
);
});
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(
<FlagsmithProvider flagsmith={flagsmith} options={initConfig}>
<FlagsmithPage/>
</FlagsmithProvider>
);
});
});
55 changes: 55 additions & 0 deletions test/types.test.ts
Original file line number Diff line number Diff line change
@@ -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")
});
});
40 changes: 32 additions & 8 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ export type DynatraceObject = {
"shortString": Record<string, string>,
"javaDouble": Record<string, number>,
}
export interface IFlagsmithFeature {
id?: number;

export interface IFlagsmithFeature<Value = IFlagsmithValue> {
id: numbers
enabled: boolean;
value?: IFlagsmithValue;
value: Value;
}

export declare type IFlagsmithTrait = IFlagsmithValue | TraitEvaluationContext;
Expand Down Expand Up @@ -132,8 +133,28 @@ export interface IFlagsmithResponse {
};
}[];
}

export interface IFlagsmith<F extends string = string, T extends string = string> {
type FKey<F> = F extends string ? F : keyof F;
type FValue<F, K extends FKey<F>> = 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<MyFeatureInterface>;
*/
export interface IFlagsmith<
F extends string | Record<string, any> = string,
T extends string = string
>
{
/**
* Initialise the sdk against a particular environment
*/
Expand Down Expand Up @@ -194,7 +215,7 @@ export interface IFlagsmith<F extends string = string, T extends string = string
* @example
* flagsmith.hasFeature("enabled_by_default_feature", { fallback: true })
*/
hasFeature: (key: F, optionsOrSkipAnalytics?: HasFeatureOptions) => boolean;
hasFeature: (key: FKey<F>, optionsOrSkipAnalytics?: HasFeatureOptions) => boolean;

/**
* Returns the value of a feature, or a fallback value.
Expand All @@ -212,8 +233,11 @@ export interface IFlagsmith<F extends string = string, T extends string = string
* flagsmith.getValue("font_size") // "12px"
* flagsmith.getValue("font_size", { json: true, fallback: "8px" }) // "8px"
*/
getValue<T = IFlagsmithValue>(key: F, options?: GetValueOptions<T>, skipAnalytics?: boolean): IFlagsmithValue<T>;

getValue<K extends FKey<F>>(
key: K,
options?: GetValueOptions<FValue<F, K>>,
skipAnalytics?: boolean
): IFlagsmithValue<FValue<F, K>>;
/**
* Get the value of a particular trait for the identified user
*/
Expand Down