You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As TypeScript has evolved, a lot of libraries have moved from not just type safety of the library itself, but also towards type-safe integration of the consuming application. E.g. TanStack Router is aware of routing configuration, Apollo Client can pull type safety of operations etc.
OpenFeature has an experimental way to add type safety for the flags themselves through the CLI. But unlike the ones listed above, this takes the approach of abstracting over the top of base library and generating typesafe runtime code. Apollo moved way from their similar approach as it generated a lot of (potentially unused) code and the APIs needed to be kept in sync.
An alternative: module augmentation
One benefit TypeScript has over the other OF libs is that we can externally change a library's types, with module augmentation. This is how TanStack Router achieves its type safety.
My proposal is to make changes in 2 places:
In the libraries themselves, all the flag getters infer a generic parameter, which is a string literal union of available flag keys
This must be opt-in, so plain string flags would be accepted
Application-side code can then augment an interface, which is how these generic parameters are populated.
Generic flag getters
To start with, the library needs to be capable of detecting whether or not an interface has been augmented, and then returning a basic fallback if it hasn't.
RegisteredFlags checks if the interface Register has a specific field (which will only be true if somewhere else has augmented it). If it has, then TFlags is inferred from this augmented interface. If not, then we fallback to the default (for simplicity, this demo only considered primitives).
Now we have our generic, we can use it in the flag evaluations:
In an augmented version, FlagKey would resolve to a string literally union of possible flags. This means the compiler will error if the developer attempts to provide a non-existent flag. It also knows the return type/default value type, to aid the developer. Without augmentation, it behaves as it does now, accepting a string.
As we know the type of our flags, we can take this a step further, and further narrow the possible flags when using type-specific flag getters:
typeBooleanFlags={[KinkeyofRegisteredFlags]: RegisteredFlags[K]extendsboolean ? K : never;}[keyofRegisteredFlags];exportfunctiongetBooleanFlag<FlagKeyextendsBooleanFlags>(flagKey: FlagKey,defaultValue: boolean): boolean{returnclient.evaluate();}
Calling this function with a valid registered flag, but one which is not a boolean, will now error.
Registering flags
On the application side, all we need to do is simply augment the library's Register interface with possible flags and their types.
Now the library becomes type aware of these 2 flags, of different types. If more flag information was required, the values could become objects rather than primitives.
Some example usage:
// ✅ valid - `bool` exists as a possible flag. The LSP provides the available options for DX// `value` is inferred to be a booleanconstvalue=getFlag('bool',true);// ❌ compiler error - `unknown` is not a registered flagconstvalue=getFlag('unknown',true);// ❌ compiler error - `bool` is registered, but is of type boolean, so the default value is invalidconstvalue=getFlag('bool','true');// ✅ valid - `bool` is a boolean flag. As before the LSP presents the available optionsconstvalue=getBooleanFlag('bool',true);// ❌ compiler error - `str` is not a boolean flagconstvalue=getBooleanFlag('str',true);
This example still uses a manually created interface in the application (which is likely still an improvement in a lot of cases). Ideally this would be populated in a similar manner to the OF FLI. I played around with using the schema.json file directly, but unfortunately TypeScript infers every field as mutable with JSON imports, so it does offer the strictness we'd need. So instead perhaps the OF CLI could generate the Register interface, as an alternative to all the generated functions?
The goal behind this proposal is to keep the type safe version maintaining an identical (albeit stricter) API to the default version, so it then directly matches documentation. It makes adoption significantly easier as larger applications won't need to go and update all their function calls, and should significantly reduce the volume of generated code.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
As TypeScript has evolved, a lot of libraries have moved from not just type safety of the library itself, but also towards type-safe integration of the consuming application. E.g. TanStack Router is aware of routing configuration, Apollo Client can pull type safety of operations etc.
OpenFeature has an experimental way to add type safety for the flags themselves through the CLI. But unlike the ones listed above, this takes the approach of abstracting over the top of base library and generating typesafe runtime code. Apollo moved way from their similar approach as it generated a lot of (potentially unused) code and the APIs needed to be kept in sync.
An alternative: module augmentation
One benefit TypeScript has over the other OF libs is that we can externally change a library's types, with module augmentation. This is how TanStack Router achieves its type safety.
My proposal is to make changes in 2 places:
stringflags would be acceptedGeneric flag getters
To start with, the library needs to be capable of detecting whether or not an interface has been augmented, and then returning a basic fallback if it hasn't.
RegisteredFlagschecks if the interfaceRegisterhas a specific field (which will only be true if somewhere else has augmented it). If it has, then TFlags is inferred from this augmented interface. If not, then we fallback to the default (for simplicity, this demo only considered primitives).Now we have our generic, we can use it in the flag evaluations:
In an augmented version,
FlagKeywould resolve to a string literally union of possible flags. This means the compiler will error if the developer attempts to provide a non-existent flag. It also knows the return type/default value type, to aid the developer. Without augmentation, it behaves as it does now, accepting a string.As we know the type of our flags, we can take this a step further, and further narrow the possible flags when using type-specific flag getters:
Calling this function with a valid registered flag, but one which is not a boolean, will now error.
Registering flags
On the application side, all we need to do is simply augment the library's
Registerinterface with possible flags and their types.Now the library becomes type aware of these 2 flags, of different types. If more flag information was required, the values could become objects rather than primitives.
Some example usage:
Interactive version on StackBlitz: https://stackblitz.com/edit/vitejs-vite-skrlxf9q?file=src%2Fapplication.ts
Populating the interface
This example still uses a manually created interface in the application (which is likely still an improvement in a lot of cases). Ideally this would be populated in a similar manner to the OF FLI. I played around with using the
schema.jsonfile directly, but unfortunately TypeScript infers every field as mutable with JSON imports, so it does offer the strictness we'd need. So instead perhaps the OF CLI could generate theRegisterinterface, as an alternative to all the generated functions?The goal behind this proposal is to keep the type safe version maintaining an identical (albeit stricter) API to the default version, so it then directly matches documentation. It makes adoption significantly easier as larger applications won't need to go and update all their function calls, and should significantly reduce the volume of generated code.
Beta Was this translation helpful? Give feedback.
All reactions