diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 799b3da75e66..045e15506eb1 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -595,6 +595,16 @@ export type FeatureStateValue = { type: 'int' | 'unicode' | 'bool' | 'float' } +// The trait shape from the core API, which keys its type as `value_type` +// where feature states use `type`. +export type TraitValue = { + boolean_value: boolean | null + float_value?: number | null + integer_value: number | null + string_value: string | null + value_type: 'int' | 'unicode' | 'bool' | 'float' +} + export type MultivariateOption = { id: number uuid: string diff --git a/frontend/common/utils/__tests__/featureStateToValue.test.ts b/frontend/common/utils/__tests__/featureStateToValue.test.ts index 3708551480e5..edc72c8d4592 100644 --- a/frontend/common/utils/__tests__/featureStateToValue.test.ts +++ b/frontend/common/utils/__tests__/featureStateToValue.test.ts @@ -1,4 +1,5 @@ import { featureStateToValue } from 'common/utils/featureStateToValue' +import type { FlagsmithValue, TraitValue } from 'common/types/responses' describe('featureStateToValue', () => { it.each([ @@ -10,10 +11,51 @@ describe('featureStateToValue', () => { expect(featureStateToValue(nested as never)).toBe(expected) }) - it('reads value_type when present (core traits)', () => { - expect( - featureStateToValue({ integer_value: 3, value_type: 'int' } as never), - ).toBe(3) + // Typed rather than cast, so these fail to compile if TraitValue drifts. + it.each<[string, TraitValue, FlagsmithValue]>([ + [ + 'int', + { + boolean_value: null, + integer_value: 3, + string_value: null, + value_type: 'int', + }, + 3, + ], + [ + 'float', + { + boolean_value: null, + float_value: 2.5, + integer_value: null, + string_value: null, + value_type: 'float', + }, + 2.5, + ], + [ + 'bool', + { + boolean_value: false, + integer_value: null, + string_value: null, + value_type: 'bool', + }, + false, + ], + [ + 'unicode', + { + boolean_value: null, + integer_value: null, + string_value: 'power_users', + value_type: 'unicode', + }, + 'power_users', + ], + ])('reads value_type on a core trait (%s)', (_label, trait, expected) => { + expect(featureStateToValue(trait)).toBe(expected) }) it('normalises a missing int/float to null', () => { diff --git a/frontend/common/utils/featureStateToValue.ts b/frontend/common/utils/featureStateToValue.ts index 730b47e336e9..9cc57ea71435 100644 --- a/frontend/common/utils/featureStateToValue.ts +++ b/frontend/common/utils/featureStateToValue.ts @@ -1,4 +1,8 @@ -import type { FeatureStateValue, FlagsmithValue } from 'common/types/responses' +import type { + FeatureStateValue, + FlagsmithValue, + TraitValue, +} from 'common/types/responses' /** * Flattens a feature state (or core trait) value into its typed scalar. @@ -10,7 +14,7 @@ import type { FeatureStateValue, FlagsmithValue } from 'common/types/responses' * their unit tests) don't pull the Flux stores in through `utils.tsx`. */ export function featureStateToValue( - value: FlagsmithValue | FeatureStateValue | undefined, + value: FlagsmithValue | FeatureStateValue | TraitValue | undefined, ): FlagsmithValue { if (value === null || value === undefined) { return null @@ -19,9 +23,7 @@ export function featureStateToValue( return value } // `value_type` is the type key on core traits; `type` on feature states. - const type = - (value as { value_type?: FeatureStateValue['type'] }).value_type ?? - value.type + const type = 'value_type' in value ? value.value_type : value.type switch (type) { case 'bool': return value.boolean_value diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 30df0c6165ae..d6564ec2b618 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -12,6 +12,7 @@ import { ProjectFlag, SegmentCondition, Tag, + TraitValue, UserPermissions, } from 'common/types/responses' import flagsmith from '@flagsmith/flagsmith' @@ -883,7 +884,7 @@ const Utils = Object.assign({}, BaseUtils, { type: 'unicode', } }, - valueToTrait(value: FlagsmithValue) { + valueToTrait(value: FlagsmithValue): TraitValue { const val = Utils.getTypedValue(value) if (typeof val === 'boolean') {