Skip to content

Commit fcc5f34

Browse files
authored
Merge pull request #298 from Flagsmith/feat/interface-definitions
feat: Add interface types
2 parents 23f90b5 + aa3ae1e commit fcc5f34

File tree

10 files changed

+240
-23
lines changed

10 files changed

+240
-23
lines changed

lib/flagsmith/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flagsmith",
3-
"version": "9.0.5",
3+
"version": "9.1.0",
44
"description": "Feature flagging to support continuous development",
55
"main": "./index.js",
66
"module": "./index.mjs",

lib/react-native-flagsmith/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-flagsmith",
3-
"version": "9.0.5",
3+
"version": "9.1.0",
44
"description": "Feature flagging to support continuous development",
55
"main": "./index.js",
66
"repository": {

react.d.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,25 @@ export declare type FlagsmithContextType<F extends string = string, T extends st
88
serverState?: IState;
99
children: React.ReactElement[] | React.ReactElement;
1010
};
11-
export declare const FlagsmithProvider: FC<FlagsmithContextType>;
12-
export declare function useFlags<F extends string, T extends string>(_flags: readonly F[], _traits?: readonly T[]): {
11+
type UseFlagsReturn<
12+
F extends string | Record<string, any>,
13+
T extends string
14+
> = F extends string
15+
? {
1316
[K in F]: IFlagsmithFeature;
1417
} & {
1518
[K in T]: IFlagsmithTrait;
19+
}
20+
: {
21+
[K in keyof F]: IFlagsmithFeature<F[K]>;
22+
} & {
23+
[K in T]: IFlagsmithTrait;
1624
};
17-
export declare const useFlagsmith: () => IFlagsmith;
25+
export declare const FlagsmithProvider: FC<FlagsmithContextType>;
26+
export declare function useFlags<
27+
F extends string | Record<string, any>,
28+
T extends string = string
29+
>(_flags: readonly F[], _traits?: readonly T[]): UseFlagsReturn<F, T>;
30+
export declare const useFlagsmith: <F extends string | Record<string, any>,
31+
T extends string = string>() => IFlagsmith<F, T>;
1832
export declare const useFlagsmithLoading: () => LoadingState | undefined;

react.tsx

+39-7
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,40 @@ export function useFlagsmithLoading() {
126126
return loadingState
127127
}
128128

129-
export function useFlags<F extends string=string, T extends string=string>(_flags: readonly F[], _traits: readonly T[] = []): {
130-
[K in F]: IFlagsmithFeature
129+
type UseFlagsReturn<
130+
F extends string | Record<string, any>,
131+
T extends string
132+
> = F extends string
133+
? {
134+
[K in F]: IFlagsmithFeature;
131135
} & {
132-
[K in T]: IFlagsmithTrait
133-
} {
136+
[K in T]: IFlagsmithTrait;
137+
}
138+
: {
139+
[K in keyof F]: IFlagsmithFeature<F[K]>;
140+
} & {
141+
[K in T]: IFlagsmithTrait;
142+
};
143+
144+
/**
145+
* Example usage:
146+
*
147+
* // A) Using string flags:
148+
* useFlags<"featureOne"|"featureTwo">(["featureOne", "featureTwo"]);
149+
*
150+
* // B) Using an object for F - this can be generated by our CLI: https://github.com/Flagsmith/flagsmith-cli :
151+
* interface MyFeatureInterface {
152+
* featureOne: string;
153+
* featureTwo: number;
154+
* }
155+
* useFlags<MyFeatureInterface>(["featureOne", "featureTwo"]);
156+
*/
157+
export function useFlags<
158+
F extends string | Record<string, any>,
159+
T extends string = string
160+
>(
161+
_flags: readonly F[], _traits: readonly T[] = []
162+
){
134163
const firstRender = useRef(true)
135164
const flags = useConstant<string[]>(flagsAsArray(_flags))
136165
const traits = useConstant<string[]>(flagsAsArray(_traits))
@@ -173,15 +202,18 @@ export function useFlags<F extends string=string, T extends string=string>(_flag
173202
return res
174203
}, [renderRef])
175204

176-
return res
205+
return res as UseFlagsReturn<F, T>
177206
}
178207

179-
export function useFlagsmith<F extends string=string, T extends string=string>() {
208+
export function useFlagsmith<
209+
F extends string | Record<string, any>,
210+
T extends string = string
211+
>() {
180212
const context = useContext(FlagsmithContext)
181213

182214
if (!context) {
183215
throw new Error('useFlagsmith must be used with in a FlagsmithProvider')
184216
}
185217

186-
return context as IFlagsmith<F, T>
218+
return context as unknown as IFlagsmith<F, T>
187219
}

test/default-flags.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Sample test
21
import { defaultState, defaultStateAlt, FLAGSMITH_KEY, getFlagsmith, getStateToCheck } from './test-constants';
32
import { IFlags } from '../types';
43

test/functions.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Sample test
21
import { getFlagsmith } from './test-constants';
32

43
describe('Flagsmith.functions', () => {

test/init.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Sample test
21
import { waitFor } from '@testing-library/react';
32
import {defaultState, FLAGSMITH_KEY, getFlagsmith, getStateToCheck, identityState} from './test-constants';
43
import { promises as fs } from 'fs';

test/react-types.test.tsx

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from 'react';
2+
import {render} from '@testing-library/react';
3+
import {FlagsmithProvider, useFlags, useFlagsmith} from '../lib/flagsmith/react';
4+
import {getFlagsmith,} from './test-constants';
5+
6+
7+
describe.only('FlagsmithProvider', () => {
8+
it('should allow supplying interface generics to useFlagsmith', () => {
9+
const FlagsmithPage = ()=> {
10+
const typedFlagsmith = useFlagsmith<
11+
{
12+
stringFlag: string
13+
numberFlag: number
14+
objectFlag: { first_name: string }
15+
}
16+
>()
17+
//@ts-expect-error - feature not defined
18+
typedFlagsmith.hasFeature("fail")
19+
//@ts-expect-error - feature not defined
20+
typedFlagsmith.getValue("fail")
21+
22+
typedFlagsmith.hasFeature("stringFlag")
23+
typedFlagsmith.hasFeature("numberFlag")
24+
typedFlagsmith.getValue("stringFlag")
25+
typedFlagsmith.getValue("numberFlag")
26+
27+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
28+
const stringFlag: string|null = typedFlagsmith.getValue("stringFlag")
29+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
30+
const numberFlag: number|null = typedFlagsmith.getValue("numberFlag")
31+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
32+
const firstName: string | undefined = typedFlagsmith.getValue("objectFlag")?.first_name
33+
34+
// @ts-expect-error - invalid does not exist on type announcement
35+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
36+
const invalidPointer: string | undefined = typedFlagsmith.getValue("objectFlag")?.invalid
37+
38+
// @ts-expect-error - feature should be a number
39+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
40+
const incorrectNumberFlag: string = typedFlagsmith.getValue("numberFlag")
41+
42+
return <></>
43+
}
44+
const onChange = jest.fn();
45+
const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange})
46+
render(
47+
<FlagsmithProvider flagsmith={flagsmith} options={initConfig}>
48+
<FlagsmithPage/>
49+
</FlagsmithProvider>
50+
);
51+
});
52+
it('should allow supplying interface generics to useFlags', () => {
53+
const FlagsmithPage = ()=> {
54+
const typedFlagsmith = useFlags<
55+
{
56+
stringFlag: string
57+
numberFlag: number
58+
objectFlag: { first_name: string }
59+
}
60+
>([])
61+
//@ts-expect-error - feature not defined
62+
typedFlagsmith.fail?.enabled
63+
//@ts-expect-error - feature not defined
64+
typedFlagsmith.fail?.value
65+
66+
typedFlagsmith.numberFlag
67+
typedFlagsmith.stringFlag
68+
typedFlagsmith.objectFlag
69+
70+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
71+
const stringFlag: string = typedFlagsmith.stringFlag?.value
72+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
73+
const numberFlag: number = typedFlagsmith.numberFlag?.value
74+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
75+
const firstName: string = typedFlagsmith.objectFlag?.value.first_name
76+
77+
// @ts-expect-error - invalid does not exist on type announcement
78+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
79+
const invalidPointer: string = typedFlagsmith.objectFlag?.value.invalid
80+
81+
// @ts-expect-error - feature should be a number
82+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
83+
const incorrectNumberFlag: string = typedFlagsmith.numberFlag?.value
84+
85+
return <></>
86+
}
87+
const onChange = jest.fn();
88+
const {flagsmith,initConfig, mockFetch} = getFlagsmith({onChange})
89+
render(
90+
<FlagsmithProvider flagsmith={flagsmith} options={initConfig}>
91+
<FlagsmithPage/>
92+
</FlagsmithProvider>
93+
);
94+
});
95+
});

test/types.test.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Sample test
2+
import {getFlagsmith} from './test-constants';
3+
import {IFlagsmith} from '../types';
4+
5+
describe('Flagsmith Types', () => {
6+
7+
// The following tests will fail to compile if any of the types fail / expect-error has no type issues
8+
// Therefor all of the following ts-expect-errors and eslint-disable-lines are by design
9+
test('should allow supplying string generics to a flagsmith instance', async () => {
10+
const { flagsmith, } = getFlagsmith({ });
11+
const typedFlagsmith = flagsmith as IFlagsmith<"flag1"|"flag2">
12+
//@ts-expect-error - feature not defined
13+
typedFlagsmith.hasFeature("fail")
14+
//@ts-expect-error - feature not defined
15+
typedFlagsmith.getValue("fail")
16+
17+
typedFlagsmith.hasFeature("flag1")
18+
typedFlagsmith.hasFeature("flag2")
19+
typedFlagsmith.getValue("flag1")
20+
typedFlagsmith.getValue("flag2")
21+
});
22+
test('should allow supplying interface generics to a flagsmith instance', async () => {
23+
const { flagsmith, } = getFlagsmith({ });
24+
const typedFlagsmith = flagsmith as IFlagsmith<
25+
{
26+
stringFlag: string
27+
numberFlag: number
28+
objectFlag: { first_name: string }
29+
}>
30+
//@ts-expect-error - feature not defined
31+
typedFlagsmith.hasFeature("fail")
32+
//@ts-expect-error - feature not defined
33+
typedFlagsmith.getValue("fail")
34+
35+
typedFlagsmith.hasFeature("stringFlag")
36+
typedFlagsmith.hasFeature("numberFlag")
37+
typedFlagsmith.getValue("stringFlag")
38+
typedFlagsmith.getValue("numberFlag")
39+
40+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
41+
const stringFlag: string | null = typedFlagsmith.getValue("stringFlag")
42+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
43+
const numberFlag: number | null = typedFlagsmith.getValue("numberFlag")
44+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
45+
const firstName: string | undefined = typedFlagsmith.getValue("objectFlag")?.first_name
46+
47+
// @ts-expect-error - invalid does not exist on type announcement
48+
//eslint-disable-next-line @typescript-eslint/no-unused-vars
49+
const invalidPointer: string = typedFlagsmith.getValue("objectFlag")?.invalid
50+
51+
// @ts-expect-error - feature should be a number
52+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
53+
const incorrectNumberFlag: string = typedFlagsmith.getValue("numberFlag")
54+
});
55+
});

types.d.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ export type DynatraceObject = {
88
"shortString": Record<string, string>,
99
"javaDouble": Record<string, number>,
1010
}
11-
export interface IFlagsmithFeature {
12-
id?: number;
11+
12+
export interface IFlagsmithFeature<Value = IFlagsmithValue> {
13+
id: numbers
1314
enabled: boolean;
14-
value?: IFlagsmithValue;
15+
value: Value;
1516
}
1617

1718
export declare type IFlagsmithTrait = IFlagsmithValue | TraitEvaluationContext;
@@ -132,8 +133,28 @@ export interface IFlagsmithResponse {
132133
};
133134
}[];
134135
}
135-
136-
export interface IFlagsmith<F extends string = string, T extends string = string> {
136+
type FKey<F> = F extends string ? F : keyof F;
137+
type FValue<F, K extends FKey<F>> = F extends string
138+
? IFlagsmithValue
139+
: F[K] | null;
140+
/**
141+
* Example usage:
142+
*
143+
* // A) Using string flags:
144+
* import flagsmith from 'flagsmith' as IFlagsmith<"featureOne"|"featureTwo">;
145+
*
146+
* // B) Using an object for F - this can be generated by our CLI: https://github.com/Flagsmith/flagsmith-cli :
147+
* interface MyFeatureInterface {
148+
* featureOne: string;
149+
* featureTwo: number;
150+
* }
151+
* import flagsmith from 'flagsmith' as IFlagsmith<MyFeatureInterface>;
152+
*/
153+
export interface IFlagsmith<
154+
F extends string | Record<string, any> = string,
155+
T extends string = string
156+
>
157+
{
137158
/**
138159
* Initialise the sdk against a particular environment
139160
*/
@@ -194,7 +215,7 @@ export interface IFlagsmith<F extends string = string, T extends string = string
194215
* @example
195216
* flagsmith.hasFeature("enabled_by_default_feature", { fallback: true })
196217
*/
197-
hasFeature: (key: F, optionsOrSkipAnalytics?: HasFeatureOptions) => boolean;
218+
hasFeature: (key: FKey<F>, optionsOrSkipAnalytics?: HasFeatureOptions) => boolean;
198219

199220
/**
200221
* Returns the value of a feature, or a fallback value.
@@ -212,8 +233,11 @@ export interface IFlagsmith<F extends string = string, T extends string = string
212233
* flagsmith.getValue("font_size") // "12px"
213234
* flagsmith.getValue("font_size", { json: true, fallback: "8px" }) // "8px"
214235
*/
215-
getValue<T = IFlagsmithValue>(key: F, options?: GetValueOptions<T>, skipAnalytics?: boolean): IFlagsmithValue<T>;
216-
236+
getValue<K extends FKey<F>>(
237+
key: K,
238+
options?: GetValueOptions<FValue<F, K>>,
239+
skipAnalytics?: boolean
240+
): IFlagsmithValue<FValue<F, K>>;
217241
/**
218242
* Get the value of a particular trait for the identified user
219243
*/

0 commit comments

Comments
 (0)