diff --git a/lib/shared/types/src/index.ts b/lib/shared/types/src/index.ts index 6d4e99b02..9b7d3b756 100644 --- a/lib/shared/types/src/index.ts +++ b/lib/shared/types/src/index.ts @@ -10,3 +10,4 @@ export * from './types/config/models' export * from './utils' export * from './types/ConfigSource' export * from './types/UserError' +export * from './types/variableKeys' diff --git a/lib/shared/types/src/types/variableKeys.ts b/lib/shared/types/src/types/variableKeys.ts new file mode 100644 index 000000000..e8be6a3c7 --- /dev/null +++ b/lib/shared/types/src/types/variableKeys.ts @@ -0,0 +1,46 @@ +import { VariableTypeAlias, VariableValue } from './config/models' + +/** + * Used to support strong typing of variable keys in the SDK. + * Usage; + * ```ts + * import '@devcycle/types'; + * declare module '@devcycle/types' { + * interface CustomVariableDefinitions { + * 'flag-one': boolean; + * } + * } + * ``` + * Or when using the cli generated types; + * ```ts + * import '@devcycle/types'; + * declare module '@devcycle/types' { + * interface CustomVariableDefinitions extends DVCVariableTypes {} + * } + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CustomVariableDefinitions {} +type DynamicBaseVariableDefinitions = + keyof CustomVariableDefinitions extends never + ? { + [key: string]: VariableValue + } + : CustomVariableDefinitions +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface VariableDefinitions extends DynamicBaseVariableDefinitions {} +export type VariableKey = string & keyof VariableDefinitions + +// type that determines whether the CustomVariableDefinitions interface has any keys defined, meaning +// that we're using custom variable types +export type CustomVariablesDefined = + keyof CustomVariableDefinitions extends never ? false : true + +// type helper which turns a default value type into the type defined in custom variable types, if those exist +// otherwise run it through VariableTypeAlias +export type InferredVariableType< + K extends VariableKey, + DefaultValue extends VariableDefinitions[K], +> = CustomVariablesDefined extends true + ? VariableDefinitions[K] + : VariableTypeAlias diff --git a/sdk/js-cloud-server/src/cloudClient.ts b/sdk/js-cloud-server/src/cloudClient.ts index ef4f3fe01..e187d2e5c 100644 --- a/sdk/js-cloud-server/src/cloudClient.ts +++ b/sdk/js-cloud-server/src/cloudClient.ts @@ -15,8 +15,9 @@ import { DevCycleServerSDKOptions, DVCLogger, getVariableTypeFromValue, + InferredVariableType, + VariableDefinitions, VariableTypeAlias, - type VariableValue, } from '@devcycle/types' import { getAllFeatures, @@ -68,10 +69,6 @@ const throwIfUserError = (err: unknown) => { throw err } -export interface VariableDefinitions { - [key: string]: VariableValue -} - export class DevCycleCloudClient< Variables extends VariableDefinitions = VariableDefinitions, > { @@ -163,7 +160,7 @@ export class DevCycleCloudClient< user: DevCycleUser, key: K, defaultValue: T, - ): Promise> { + ): Promise> { return (await this.variable(user, key, defaultValue)).value } diff --git a/sdk/js-cloud-server/src/models/variable.ts b/sdk/js-cloud-server/src/models/variable.ts index abbe2fe81..09f77dc0f 100644 --- a/sdk/js-cloud-server/src/models/variable.ts +++ b/sdk/js-cloud-server/src/models/variable.ts @@ -1,4 +1,9 @@ -import { VariableType, VariableTypeAlias } from '@devcycle/types' +import { + InferredVariableType, + VariableKey, + VariableType, + VariableTypeAlias, +} from '@devcycle/types' import { DVCVariableInterface, DVCVariableValue } from '../types' import { checkParamDefined, @@ -14,11 +19,13 @@ export type VariableParam = { evalReason?: unknown } -export class DVCVariable - implements DVCVariableInterface +export class DVCVariable< + T extends DVCVariableValue, + K extends VariableKey = VariableKey, +> implements DVCVariableInterface { - key: string - value: VariableTypeAlias + key: K + value: InferredVariableType readonly defaultValue: T readonly isDefaulted: boolean readonly type: 'String' | 'Number' | 'Boolean' | 'JSON' @@ -29,7 +36,9 @@ export class DVCVariable checkParamDefined('key', key) checkParamDefined('defaultValue', defaultValue) checkParamType('key', key, typeEnum.string) - this.key = key.toLowerCase() + // kind of cheating here with the type assertion but we're basically assuming that all variable keys in + // generated types are lowercase since the system enforces that elsewhere + this.key = key.toLowerCase() as K this.isDefaulted = value === undefined || value === null this.value = value === undefined || value === null diff --git a/sdk/js-cloud-server/src/request.ts b/sdk/js-cloud-server/src/request.ts index 33f44f82a..3c2d48dbc 100644 --- a/sdk/js-cloud-server/src/request.ts +++ b/sdk/js-cloud-server/src/request.ts @@ -2,7 +2,6 @@ import { DVCPopulatedUser } from './models/populatedUser' import { DevCycleEvent } from './types' import { DevCycleServerSDKOptions } from '@devcycle/types' import { post } from '@devcycle/server-request' -import { VariableKey } from '@devcycle/js-client-sdk' export const HOST = '.devcycle.com' @@ -59,7 +58,7 @@ export async function getAllVariables( export async function getVariable( user: DVCPopulatedUser, sdkKey: string, - variableKey: VariableKey, + variableKey: string, options: DevCycleServerSDKOptions, ): Promise { const baseUrl = `${ diff --git a/sdk/js/src/Client.ts b/sdk/js/src/Client.ts index 4923b1c79..6e48c09b0 100644 --- a/sdk/js/src/Client.ts +++ b/sdk/js/src/Client.ts @@ -8,7 +8,6 @@ import { DevCycleUser, ErrorCallback, DVCFeature, - VariableDefinitions, UserError, } from './types' @@ -20,7 +19,12 @@ import { DVCPopulatedUser } from './User' import { EventQueue, EventTypes } from './EventQueue' import { checkParamDefined } from './utils' import { EventEmitter } from './EventEmitter' -import type { BucketedUserConfig, VariableTypeAlias } from '@devcycle/types' +import type { + BucketedUserConfig, + InferredVariableType, + VariableDefinitions, + VariableTypeAlias, +} from '@devcycle/types' import { getVariableTypeFromValue } from '@devcycle/types' import { ConfigRequestConsolidator } from './ConfigRequestConsolidator' import { dvcDefaultLogger } from './logger' diff --git a/sdk/js/src/EventQueue.ts b/sdk/js/src/EventQueue.ts index 77b44a821..157345ba0 100644 --- a/sdk/js/src/EventQueue.ts +++ b/sdk/js/src/EventQueue.ts @@ -1,13 +1,9 @@ import { DevCycleClient } from './Client' -import { - DevCycleEvent, - DevCycleOptions, - VariableDefinitions, - DVCCustomDataJSON, -} from './types' +import { DevCycleEvent, DevCycleOptions, DVCCustomDataJSON } from './types' import { publishEvents } from './Request' import { checkParamDefined } from './utils' import chunk from 'lodash/chunk' +import { VariableDefinitions } from '@devcycle/types' export const EventTypes = { variableEvaluated: 'variableEvaluated', diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 840091dbf..714e4d5d0 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -2,7 +2,6 @@ import { DevCycleEvent, DevCycleOptions, DevCycleUser, - VariableDefinitions, UserError, DVCCustomDataJSON, } from './types' @@ -15,6 +14,9 @@ import { checkIsServiceWorker } from './utils' export * from './types' export { dvcDefaultLogger } from './logger' +import { VariableDefinitions } from '@devcycle/types' +export { VariableDefinitions } + /** * @deprecated Use DevCycleClient instead */ diff --git a/sdk/js/src/types.ts b/sdk/js/src/types.ts index ed0a17500..d4e931626 100644 --- a/sdk/js/src/types.ts +++ b/sdk/js/src/types.ts @@ -7,6 +7,8 @@ import type { DevCycleJSON, DVCCustomDataJSON, BucketedUserConfig, + VariableKey, + InferredVariableType, } from '@devcycle/types' export { UserError } from '@devcycle/types' @@ -193,38 +195,10 @@ export interface DevCycleUser { privateCustomData?: T } -/** - * Used to support strong typing of flag strings in the SDK. - * Usage; - * ```ts - * import '@devcycle/js-client-sdk'; - * declare module '@devcycle/js-client-sdk' { - * interface CustomVariableDefinitions { - * 'flag-one': boolean; - * } - * } - * ``` - * Or when using the cli generated types; - * ```ts - * import '@devcycle/js-client-sdk'; - * declare module '@devcycle/js-client-sdk' { - * interface CustomVariableDefinitions extends DVCVariableTypes {} - * } - * ``` - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomVariableDefinitions {} -type DynamicBaseVariableDefinitions = - keyof CustomVariableDefinitions extends never - ? { - [key: string]: VariableValue - } - : CustomVariableDefinitions -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface VariableDefinitions extends DynamicBaseVariableDefinitions {} -export type VariableKey = string & keyof VariableDefinitions - -export interface DVCVariable { +export interface DVCVariable< + T extends DVCVariableValue, + K extends VariableKey = VariableKey, +> { /** * Unique "key" by Project to use for this Dynamic Variable. */ @@ -234,7 +208,7 @@ export interface DVCVariable { * The value for this Dynamic Variable which will be set to the `defaultValue` * if accessed before the SDK is fully Initialized */ - readonly value: VariableTypeAlias + readonly value: InferredVariableType /** * Default value set when creating the variable diff --git a/sdk/nestjs/src/DevCycleModule/DevCycleService.ts b/sdk/nestjs/src/DevCycleModule/DevCycleService.ts index 6deaccbb7..fa568ec72 100644 --- a/sdk/nestjs/src/DevCycleModule/DevCycleService.ts +++ b/sdk/nestjs/src/DevCycleModule/DevCycleService.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common' +import { DevCycleClient, DevCycleUser } from '@devcycle/nodejs-server-sdk' import { - DVCVariableValue, - DevCycleClient, - DevCycleUser, -} from '@devcycle/nodejs-server-sdk' -import { VariableTypeAlias } from '@devcycle/types' + InferredVariableType, + VariableDefinitions, + VariableKey, +} from '@devcycle/types' import { ClsService } from 'nestjs-cls' @Injectable() @@ -18,14 +18,14 @@ export class DevCycleService { return this.cls.get('dvc_user') } - isEnabled(key: string): boolean { + isEnabled(key: VariableKey): boolean { return this.devcycleClient.variableValue(this.getUser(), key, false) } - variableValue( - key: string, - defaultValue: T, - ): VariableTypeAlias { + variableValue< + K extends VariableKey, + ValueType extends VariableDefinitions[K], + >(key: K, defaultValue: ValueType): InferredVariableType { return this.devcycleClient.variableValue( this.getUser(), key, diff --git a/sdk/nextjs/src/client/useVariableValue.ts b/sdk/nextjs/src/client/useVariableValue.ts index 1bba57914..106a70c64 100644 --- a/sdk/nextjs/src/client/useVariableValue.ts +++ b/sdk/nextjs/src/client/useVariableValue.ts @@ -1,15 +1,18 @@ 'use client' -import { DevCycleClient, DVCVariableValue } from '@devcycle/js-client-sdk' +import { DevCycleClient } from '@devcycle/js-client-sdk' import { useContext, use } from 'react' -import { VariableTypeAlias } from '@devcycle/types' +import { VariableDefinitions, VariableKey } from '@devcycle/types' import { DVCVariable } from '@devcycle/js-client-sdk' import { DevCycleProviderContext } from './internal/context' import { useRerenderOnVariableChange } from './internal/useRerenderOnVariableChange' -export const useVariable = ( - key: string, - defaultValue: T, -): DVCVariable => { +export const useVariable = < + K extends VariableKey, + ValueType extends VariableDefinitions[K], +>( + key: K, + defaultValue: ValueType, +): DVCVariable => { const context = useContext(DevCycleProviderContext) useRerenderOnVariableChange(key) @@ -21,10 +24,13 @@ export const useVariable = ( return context.client.variable(key, defaultValue) } -export const useVariableValue = ( - key: string, - defaultValue: T, -): VariableTypeAlias => { +export const useVariableValue = < + K extends VariableKey, + ValueType extends VariableDefinitions[K], +>( + key: K, + defaultValue: ValueType, +): DVCVariable['value'] => { return useVariable(key, defaultValue).value } diff --git a/sdk/nextjs/src/server/getVariableValue.ts b/sdk/nextjs/src/server/getVariableValue.ts index 93fee076e..21c8056f1 100644 --- a/sdk/nextjs/src/server/getVariableValue.ts +++ b/sdk/nextjs/src/server/getVariableValue.ts @@ -1,17 +1,20 @@ import { getClient } from './requestContext' -import { DVCVariableValue } from '@devcycle/js-client-sdk' -import { VariableTypeAlias } from '@devcycle/types' +import { + VariableDefinitions, + VariableKey, + VariableTypeAlias, +} from '@devcycle/types' -export async function getVariableValue( - key: string, - defaultValue: T, -): Promise> { +export async function getVariableValue< + K extends VariableKey, + ValueType extends VariableDefinitions[K], +>(key: K, defaultValue: ValueType): Promise> { const client = getClient() if (!client) { console.error( 'React cache API is not working as expected. Please contact DevCycle support.', ) - return defaultValue as VariableTypeAlias + return defaultValue as VariableTypeAlias } const variable = client.variable(key, defaultValue) diff --git a/sdk/nodejs/src/client.ts b/sdk/nodejs/src/client.ts index 1825c436e..ca3530edb 100644 --- a/sdk/nodejs/src/client.ts +++ b/sdk/nodejs/src/client.ts @@ -15,8 +15,9 @@ import { DVCLogger, getVariableTypeFromValue, VariableTypeAlias, - type VariableValue, UserError, + VariableDefinitions, + InferredVariableType, } from '@devcycle/types' import os from 'os' import { @@ -56,10 +57,6 @@ type DevCycleProviderConstructor = typeof import('./open-feature/DevCycleProvider').DevCycleProvider type DevCycleProvider = InstanceType -export interface VariableDefinitions { - [key: string]: VariableValue -} - export class DevCycleClient< Variables extends VariableDefinitions = VariableDefinitions, > { @@ -290,7 +287,7 @@ export class DevCycleClient< variableValue< K extends string & keyof Variables, T extends DVCVariableValue & Variables[K], - >(user: DevCycleUser, key: K, defaultValue: T): VariableTypeAlias { + >(user: DevCycleUser, key: K, defaultValue: T): InferredVariableType { return this.variable(user, key, defaultValue).value } diff --git a/sdk/nodejs/src/index.ts b/sdk/nodejs/src/index.ts index 57e865487..bb9a78177 100644 --- a/sdk/nodejs/src/index.ts +++ b/sdk/nodejs/src/index.ts @@ -16,8 +16,7 @@ import { DVCFeatureSet, DevCyclePlatformDetails, } from '@devcycle/js-cloud-server-sdk' -import { VariableDefinitions } from '@devcycle/js-client-sdk' -import { DevCycleServerSDKOptions } from '@devcycle/types' +import { DevCycleServerSDKOptions, VariableDefinitions } from '@devcycle/types' import { getNodeJSPlatformDetails } from './utils/platformDetails' // Dynamically import the OpenFeature Provider, as it's an optional peer dependency diff --git a/sdk/react/src/RenderIfEnabled.tsx b/sdk/react/src/RenderIfEnabled.tsx index 1d807027b..d668f67f3 100644 --- a/sdk/react/src/RenderIfEnabled.tsx +++ b/sdk/react/src/RenderIfEnabled.tsx @@ -1,7 +1,8 @@ import useVariableValue from './useVariableValue' -import { DVCVariableValue, VariableKey } from '@devcycle/js-client-sdk' +import { DVCVariableValue } from '@devcycle/js-client-sdk' import { useContext } from 'react' import { debugContext } from './context' +import { VariableKey } from '@devcycle/types' type CommonProps = { children: React.ReactNode diff --git a/sdk/react/src/SwapComponents.tsx b/sdk/react/src/SwapComponents.tsx index f1a3a4a9d..f9fe1d7c6 100644 --- a/sdk/react/src/SwapComponents.tsx +++ b/sdk/react/src/SwapComponents.tsx @@ -1,5 +1,5 @@ import { ComponentProps, ComponentType } from 'react' -import type { VariableKey } from '@devcycle/js-client-sdk' +import type { VariableKey } from '@devcycle/types' import useVariableValue from './useVariableValue' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types diff --git a/sdk/react/src/useVariable.ts b/sdk/react/src/useVariable.ts index 5787dba3c..7d9fc705f 100644 --- a/sdk/react/src/useVariable.ts +++ b/sdk/react/src/useVariable.ts @@ -1,16 +1,16 @@ 'use client' import { useCallback, useContext, useEffect, useState } from 'react' import context from './context' -import type { - DVCVariable, - DVCVariableValue, - VariableKey, -} from '@devcycle/js-client-sdk' +import type { DVCVariable } from '@devcycle/js-client-sdk' +import { VariableDefinitions, VariableKey } from '@devcycle/types' -export const useVariable = ( - key: VariableKey, - defaultValue: T, -): DVCVariable => { +export const useVariable = < + K extends VariableKey, + ValueType extends VariableDefinitions[K], +>( + key: K, + defaultValue: ValueType, +): DVCVariable => { const dvcContext = useContext(context) const [_, forceRerender] = useState({}) const forceRerenderCallback = useCallback(() => forceRerender({}), []) diff --git a/sdk/react/src/useVariableValue.ts b/sdk/react/src/useVariableValue.ts index 371cda27c..9c669da1a 100644 --- a/sdk/react/src/useVariableValue.ts +++ b/sdk/react/src/useVariableValue.ts @@ -1,11 +1,17 @@ import { useVariable } from './useVariable' -import type { DVCVariableValue, VariableKey } from '@devcycle/js-client-sdk' -import type { VariableTypeAlias } from '@devcycle/types' +import { + InferredVariableType, + VariableDefinitions, + VariableKey, +} from '@devcycle/types' -export const useVariableValue = ( - key: VariableKey, - defaultValue: T, -): VariableTypeAlias => { +export const useVariableValue = < + K extends VariableKey, + DefaultValue extends VariableDefinitions[K], +>( + key: K, + defaultValue: DefaultValue, +): InferredVariableType => { return useVariable(key, defaultValue).value }