Skip to content

Commit 5ea2ba0

Browse files
ajwoottoSimeonC
andauthored
feat: make variable key types overridable via module declaration (#1002)
Co-authored-by: SimeonC <[email protected]>
1 parent 38ba179 commit 5ea2ba0

File tree

17 files changed

+157
-76
lines changed

17 files changed

+157
-76
lines changed

lib/shared/types/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './types/config/models'
1010
export * from './utils'
1111
export * from './types/ConfigSource'
1212
export * from './types/UserError'
13+
export * from './types/variableKeys'
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { VariableTypeAlias, VariableValue } from './config/models'
2+
3+
/**
4+
* Used to support strong typing of variable keys in the SDK.
5+
* Usage;
6+
* ```ts
7+
* import '@devcycle/types';
8+
* declare module '@devcycle/types' {
9+
* interface CustomVariableDefinitions {
10+
* 'flag-one': boolean;
11+
* }
12+
* }
13+
* ```
14+
* Or when using the cli generated types;
15+
* ```ts
16+
* import '@devcycle/types';
17+
* declare module '@devcycle/types' {
18+
* interface CustomVariableDefinitions extends DVCVariableTypes {}
19+
* }
20+
* ```
21+
*/
22+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
23+
export interface CustomVariableDefinitions {}
24+
type DynamicBaseVariableDefinitions =
25+
keyof CustomVariableDefinitions extends never
26+
? {
27+
[key: string]: VariableValue
28+
}
29+
: CustomVariableDefinitions
30+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
31+
export interface VariableDefinitions extends DynamicBaseVariableDefinitions {}
32+
export type VariableKey = string & keyof VariableDefinitions
33+
34+
// type that determines whether the CustomVariableDefinitions interface has any keys defined, meaning
35+
// that we're using custom variable types
36+
export type CustomVariablesDefined =
37+
keyof CustomVariableDefinitions extends never ? false : true
38+
39+
// type helper which turns a default value type into the type defined in custom variable types, if those exist
40+
// otherwise run it through VariableTypeAlias
41+
export type InferredVariableType<
42+
K extends VariableKey,
43+
DefaultValue extends VariableDefinitions[K],
44+
> = CustomVariablesDefined extends true
45+
? VariableDefinitions[K]
46+
: VariableTypeAlias<DefaultValue>

sdk/js-cloud-server/src/cloudClient.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
DevCycleServerSDKOptions,
1616
DVCLogger,
1717
getVariableTypeFromValue,
18+
InferredVariableType,
19+
VariableDefinitions,
1820
VariableTypeAlias,
19-
type VariableValue,
2021
} from '@devcycle/types'
2122
import {
2223
getAllFeatures,
@@ -68,10 +69,6 @@ const throwIfUserError = (err: unknown) => {
6869
throw err
6970
}
7071

71-
export interface VariableDefinitions {
72-
[key: string]: VariableValue
73-
}
74-
7572
export class DevCycleCloudClient<
7673
Variables extends VariableDefinitions = VariableDefinitions,
7774
> {
@@ -163,7 +160,7 @@ export class DevCycleCloudClient<
163160
user: DevCycleUser,
164161
key: K,
165162
defaultValue: T,
166-
): Promise<VariableTypeAlias<T>> {
163+
): Promise<InferredVariableType<K, T>> {
167164
return (await this.variable(user, key, defaultValue)).value
168165
}
169166

sdk/js-cloud-server/src/models/variable.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { VariableType, VariableTypeAlias } from '@devcycle/types'
1+
import {
2+
InferredVariableType,
3+
VariableKey,
4+
VariableType,
5+
VariableTypeAlias,
6+
} from '@devcycle/types'
27
import { DVCVariableInterface, DVCVariableValue } from '../types'
38
import {
49
checkParamDefined,
@@ -14,11 +19,13 @@ export type VariableParam<T extends DVCVariableValue> = {
1419
evalReason?: unknown
1520
}
1621

17-
export class DVCVariable<T extends DVCVariableValue>
18-
implements DVCVariableInterface
22+
export class DVCVariable<
23+
T extends DVCVariableValue,
24+
K extends VariableKey = VariableKey,
25+
> implements DVCVariableInterface
1926
{
20-
key: string
21-
value: VariableTypeAlias<T>
27+
key: K
28+
value: InferredVariableType<K, T>
2229
readonly defaultValue: T
2330
readonly isDefaulted: boolean
2431
readonly type: 'String' | 'Number' | 'Boolean' | 'JSON'
@@ -29,7 +36,9 @@ export class DVCVariable<T extends DVCVariableValue>
2936
checkParamDefined('key', key)
3037
checkParamDefined('defaultValue', defaultValue)
3138
checkParamType('key', key, typeEnum.string)
32-
this.key = key.toLowerCase()
39+
// kind of cheating here with the type assertion but we're basically assuming that all variable keys in
40+
// generated types are lowercase since the system enforces that elsewhere
41+
this.key = key.toLowerCase() as K
3342
this.isDefaulted = value === undefined || value === null
3443
this.value =
3544
value === undefined || value === null

sdk/js/src/Client.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
DevCycleUser,
99
ErrorCallback,
1010
DVCFeature,
11-
VariableDefinitions,
1211
UserError,
1312
} from './types'
1413

@@ -20,7 +19,12 @@ import { DVCPopulatedUser } from './User'
2019
import { EventQueue, EventTypes } from './EventQueue'
2120
import { checkParamDefined } from './utils'
2221
import { EventEmitter } from './EventEmitter'
23-
import type { BucketedUserConfig, VariableTypeAlias } from '@devcycle/types'
22+
import type {
23+
BucketedUserConfig,
24+
InferredVariableType,
25+
VariableDefinitions,
26+
VariableTypeAlias,
27+
} from '@devcycle/types'
2428
import { getVariableTypeFromValue } from '@devcycle/types'
2529
import { ConfigRequestConsolidator } from './ConfigRequestConsolidator'
2630
import { dvcDefaultLogger } from './logger'

sdk/js/src/EventQueue.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { DevCycleClient } from './Client'
2-
import {
3-
DevCycleEvent,
4-
DevCycleOptions,
5-
VariableDefinitions,
6-
DVCCustomDataJSON,
7-
} from './types'
2+
import { DevCycleEvent, DevCycleOptions, DVCCustomDataJSON } from './types'
83
import { publishEvents } from './Request'
94
import { checkParamDefined } from './utils'
105
import chunk from 'lodash/chunk'
6+
import { VariableDefinitions } from '@devcycle/types'
117

128
export const EventTypes = {
139
variableEvaluated: 'variableEvaluated',

sdk/js/src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
DevCycleEvent,
33
DevCycleOptions,
44
DevCycleUser,
5-
VariableDefinitions,
65
UserError,
76
DVCCustomDataJSON,
87
} from './types'
@@ -15,6 +14,9 @@ import { checkIsServiceWorker } from './utils'
1514
export * from './types'
1615
export { dvcDefaultLogger } from './logger'
1716

17+
import { VariableDefinitions } from '@devcycle/types'
18+
export { VariableDefinitions }
19+
1820
/**
1921
* @deprecated Use DevCycleClient instead
2022
*/

sdk/js/src/types.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
DevCycleJSON,
88
DVCCustomDataJSON,
99
BucketedUserConfig,
10+
VariableKey,
11+
InferredVariableType,
1012
} from '@devcycle/types'
1113
export { UserError } from '@devcycle/types'
1214

@@ -193,21 +195,20 @@ export interface DevCycleUser<T extends DVCCustomDataJSON = DVCCustomDataJSON> {
193195
privateCustomData?: T
194196
}
195197

196-
export interface VariableDefinitions {
197-
[key: string]: VariableValue
198-
}
199-
200-
export interface DVCVariable<T extends DVCVariableValue> {
198+
export interface DVCVariable<
199+
T extends DVCVariableValue,
200+
K extends VariableKey = VariableKey,
201+
> {
201202
/**
202203
* Unique "key" by Project to use for this Dynamic Variable.
203204
*/
204-
readonly key: string
205+
readonly key: VariableKey
205206

206207
/**
207208
* The value for this Dynamic Variable which will be set to the `defaultValue`
208209
* if accessed before the SDK is fully Initialized
209210
*/
210-
readonly value: VariableTypeAlias<T>
211+
readonly value: InferredVariableType<K, T>
211212

212213
/**
213214
* Default value set when creating the variable

sdk/nestjs/src/DevCycleModule/DevCycleService.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Injectable } from '@nestjs/common'
2+
import { DevCycleClient, DevCycleUser } from '@devcycle/nodejs-server-sdk'
23
import {
3-
DVCVariableValue,
4-
DevCycleClient,
5-
DevCycleUser,
6-
} from '@devcycle/nodejs-server-sdk'
7-
import { VariableTypeAlias } from '@devcycle/types'
4+
InferredVariableType,
5+
VariableDefinitions,
6+
VariableKey,
7+
} from '@devcycle/types'
88
import { ClsService } from 'nestjs-cls'
99

1010
@Injectable()
@@ -18,14 +18,14 @@ export class DevCycleService {
1818
return this.cls.get('dvc_user')
1919
}
2020

21-
isEnabled(key: string): boolean {
21+
isEnabled(key: VariableKey): boolean {
2222
return this.devcycleClient.variableValue(this.getUser(), key, false)
2323
}
2424

25-
variableValue<T extends DVCVariableValue>(
26-
key: string,
27-
defaultValue: T,
28-
): VariableTypeAlias<T> {
25+
variableValue<
26+
K extends VariableKey,
27+
ValueType extends VariableDefinitions[K],
28+
>(key: K, defaultValue: ValueType): InferredVariableType<K, ValueType> {
2929
return this.devcycleClient.variableValue(
3030
this.getUser(),
3131
key,

sdk/nextjs/src/client/useVariableValue.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
'use client'
2-
import { DevCycleClient, DVCVariableValue } from '@devcycle/js-client-sdk'
2+
import { DevCycleClient } from '@devcycle/js-client-sdk'
33
import { useContext, use } from 'react'
4-
import { VariableTypeAlias } from '@devcycle/types'
4+
import {
5+
InferredVariableType,
6+
VariableDefinitions,
7+
VariableKey,
8+
} from '@devcycle/types'
59
import { DVCVariable } from '@devcycle/js-client-sdk'
610
import { DevCycleProviderContext } from './internal/context'
711
import { useRerenderOnVariableChange } from './internal/useRerenderOnVariableChange'
812

9-
export const useVariable = <T extends DVCVariableValue>(
10-
key: string,
11-
defaultValue: T,
12-
): DVCVariable<T> => {
13+
export const useVariable = <
14+
K extends VariableKey,
15+
ValueType extends VariableDefinitions[K],
16+
>(
17+
key: K,
18+
defaultValue: ValueType,
19+
): DVCVariable<ValueType> => {
1320
const context = useContext(DevCycleProviderContext)
1421
useRerenderOnVariableChange(key)
1522

@@ -21,10 +28,13 @@ export const useVariable = <T extends DVCVariableValue>(
2128
return context.client.variable(key, defaultValue)
2229
}
2330

24-
export const useVariableValue = <T extends DVCVariableValue>(
25-
key: string,
26-
defaultValue: T,
27-
): VariableTypeAlias<T> => {
31+
export const useVariableValue = <
32+
K extends VariableKey,
33+
ValueType extends VariableDefinitions[K],
34+
>(
35+
key: K,
36+
defaultValue: ValueType,
37+
): InferredVariableType<K, ValueType> => {
2838
return useVariable(key, defaultValue).value
2939
}
3040

sdk/nextjs/src/server/getVariableValue.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { getClient } from './requestContext'
2-
import { DVCVariableValue } from '@devcycle/js-client-sdk'
3-
import { VariableTypeAlias } from '@devcycle/types'
2+
import {
3+
InferredVariableType,
4+
VariableDefinitions,
5+
VariableKey,
6+
VariableTypeAlias,
7+
} from '@devcycle/types'
48

5-
export async function getVariableValue<T extends DVCVariableValue>(
6-
key: string,
7-
defaultValue: T,
8-
): Promise<VariableTypeAlias<T>> {
9+
export async function getVariableValue<
10+
K extends VariableKey,
11+
ValueType extends VariableDefinitions[K],
12+
>(
13+
key: K,
14+
defaultValue: ValueType,
15+
): Promise<InferredVariableType<K, ValueType>> {
916
const client = getClient()
1017
if (!client) {
1118
console.error(
1219
'React cache API is not working as expected. Please contact DevCycle support.',
1320
)
14-
return defaultValue as VariableTypeAlias<T>
21+
return defaultValue as VariableTypeAlias<ValueType>
1522
}
1623

1724
const variable = client.variable(key, defaultValue)

sdk/nodejs/src/client.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
DVCLogger,
1616
getVariableTypeFromValue,
1717
VariableTypeAlias,
18-
type VariableValue,
1918
UserError,
19+
VariableDefinitions,
20+
InferredVariableType,
2021
} from '@devcycle/types'
2122
import os from 'os'
2223
import {
@@ -56,10 +57,6 @@ type DevCycleProviderConstructor =
5657
typeof import('./open-feature/DevCycleProvider').DevCycleProvider
5758
type DevCycleProvider = InstanceType<DevCycleProviderConstructor>
5859

59-
export interface VariableDefinitions {
60-
[key: string]: VariableValue
61-
}
62-
6360
export class DevCycleClient<
6461
Variables extends VariableDefinitions = VariableDefinitions,
6562
> {
@@ -290,7 +287,7 @@ export class DevCycleClient<
290287
variableValue<
291288
K extends string & keyof Variables,
292289
T extends DVCVariableValue & Variables[K],
293-
>(user: DevCycleUser, key: K, defaultValue: T): VariableTypeAlias<T> {
290+
>(user: DevCycleUser, key: K, defaultValue: T): InferredVariableType<K, T> {
294291
return this.variable(user, key, defaultValue).value
295292
}
296293

sdk/nodejs/src/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import {
1616
DVCFeatureSet,
1717
DevCyclePlatformDetails,
1818
} from '@devcycle/js-cloud-server-sdk'
19-
import { VariableDefinitions } from '@devcycle/js-client-sdk'
20-
import { DevCycleServerSDKOptions } from '@devcycle/types'
19+
import { DevCycleServerSDKOptions, VariableDefinitions } from '@devcycle/types'
2120
import { getNodeJSPlatformDetails } from './utils/platformDetails'
2221

2322
// Dynamically import the OpenFeature Provider, as it's an optional peer dependency

sdk/react/src/RenderIfEnabled.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import useVariableValue from './useVariableValue'
22
import { DVCVariableValue } from '@devcycle/js-client-sdk'
33
import { useContext } from 'react'
44
import { debugContext } from './context'
5+
import { VariableKey } from '@devcycle/types'
56

67
type CommonProps = {
78
children: React.ReactNode
8-
variableKey: string
9+
variableKey: VariableKey
910
}
1011

1112
type RenderIfEnabledProps<T extends DVCVariableValue> =

sdk/react/src/SwapComponents.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ComponentProps, ComponentType } from 'react'
2+
import type { VariableKey } from '@devcycle/types'
23
import useVariableValue from './useVariableValue'
34

45
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
56
export const SwapComponents = <T extends ComponentType<any>>(
67
OldComponent: T,
78
NewComponent: T,
8-
variableKey: string,
9+
variableKey: VariableKey,
910
) => {
1011
const DevCycleConditionalComponent = (
1112
props: ComponentProps<T>,

0 commit comments

Comments
 (0)