Open enepeti opened 2 years ago
Thanks @enepeti !
I have similar problem too with the Config
system lacking of type hinting.
My suggested solution will be something similar to vite js.
We can have a file in root directory called foal-env.d.ts
which will be consumed by the Config
system
and export an interface like:
interface FoalEnv{
settings:{
serverPath:string,
...
}
}
and developers can extend their own config interface to this file.
And there are already some type utilities library to support object string path with hinting. E.g. https://millsp.github.io/ts-toolbelt/modules/function_autopath.html What do you think @LoicPoullain ?
Idea2: Wish to support typescript interface for Configfile with hinted path. For example:
Config.get<P extends FoalsDefaultConfigInterface,K extends string=never>(path:AutoPath<P,K>,...others)
Originally posted by @kingdun3284 in https://github.com/FoalTS/foal/issues/1027#issuecomment-1190155203
@kingdun3284 thank you for your reply! I'm trying to explore the possibilities of typescript, and learn as much as I can, that's why I try to implement stuff myself, not checking for already made solutions :). I'll check the library and I think if it has the same functionality, we should definitely use that instead of a diy solution.
But I'm not sure about having the foal-env.d.ts
file as it feels like you have another file that you need to maintain to have correct types in the Config.get
function instead of just adding stuff to the Config file. Also you can't have type restriction in a JSON file, so you can define keys in the d.ts
file which would not have a value in the config files.
It’s true that it is tedious not to have auto-completion for the configuration path. And having type inference could be interested as well. There are some concerns that I have through:
Path guess
based on the default.json the key parameter is typed, it only accepts strings that are defined in the config file in the same format as the Config.get method accept keys (e.g.: 'port', 'setting.debug')
As you might notice, the solution heavily relies on the default.json, if we want to read a value which is missing from that file, typescript will throw an error. In my opinion this is more of a positive outcome than a negative, as it forces to define a default value for every key that the application will try to read, also making the defaultValue parameter obsolete (defaults should be defined in default.json).
Yes, this is a limitation and I’d rather not to have to specify all the config keys in the default.json
. One situation where it is particularly handy is with the default framework config values:
settings.jwt.cookie.name
, etc) and including them all in default.json
could be a bit messy.settings.jwt.privateKey
and settings.jwt.secret
cannot co-exist.Based on this, we might want to define some parameters only for production in production.json
such a cookie domain.
Maybe the foal-env.d.ts
interface could be a solution to this. Developers then would have to also include the framework settings.
Typescript inference
Another limitation is in a json file we can only have string, boolean, number, array and object types, the type the function returns with can only be these. So if we want to have a more restricted type (e.g. Stripe API has an Interval type which is a subset of string ("weekly" | "daily" | "manual" | "monthly") and we want to use the value straight from the config) we either need to cast the type, be able to specify the return type of the function similar to the current solution, or have some utility to rewrite the type of the config (which I included in the solution).
Yes, this would not include types such as ’foo’|’bar’
or false|string[]
and will only work for all variables if all configuration is defined in default.json
Type conversion
Another point that I see here is that, by only inferring the TypeScript type, we don’t check nor convert the JS type which is currently done in the framework. For example, if we specify { settings: { debug: env(SETTINGS_DEBUG) } }
and use Config.get(‘settings.debug’, ‘boolean’)
, the value "true"
won’t be converted to a boolean and the framework won’t ever check that the configuration value has the correct (JS) type.
I'm also a bit worried about the stability of very complex TypeScript types such as AutoPath
and we should not have breaking changes introduced between two minors. These types are also difficult to test.
But maybe, we could end up on something more customizable that any one could adjust. For example, based on the example here, we could add more generic types on Config.get
:
Config.get<P extends string, V extends ExpectedTypeBasedOnSecondParameter, ...>(key: P, ...)
Config.get<AutoPath<some types>, AutoKey(some types)>()
Or maybe something like this:
function getConfig(key: AutoPath<some types>, type, ...) {
return Config.get(key, type)
}
Or maybe something like this:
function getConfig(key: AutoPath<some types>, type, ...) { return Config.get(key, type) }
It seems that AutoPath is a bit buggy in latest typescript version,what if writting a proxy object for that purpose?We can do the type casting ourself too.
let ConfigObj:FoalsEnv//interface from foals-env.d.ts
const handler=(path:string[]=[])=>({
get: (target, key) => {
const value=Config.get([...path,key].join("."));
if(typeof value==="object")
return new Proxy({},handler([...path,key]))
else return value
},
})
ConfigObj=new Proxy({},handler())
console.log(ConfigObj.settings.serverPath)
Or maybe something like this:
function getConfig(key: AutoPath<some types>, type, ...) { return Config.get(key, type) }
I have thought about another better design using proxy pattern as well.
const TARGET_SYMBOL = Symbol('proxied-target');
function toPath<T extends object>(obj: T): T {
function createProxy(path: any[] = []) {
const proxy = new Proxy(path, {
get: (target, key) => {
if (key == TARGET_SYMBOL) return target;
path.push(key);
return proxy;
},
});
return proxy;
}
return createProxy();
}
function typedConfig<T>(path: (obj: FoalsEnv) => T): T {
return Config.get((path(toPath(obj)) as any)[TARGET_SYMBOL].join("."));
}
to use it, simply
typedConfig((config)=>config.settings.serverPath)
Also, it would be better to support typescript config file as well.
I may have a way to make it work all together. With this solution the goals are to:
enum
validation in the future.Each Foal project would have a new src/config.ts
where we would be able to define the schema of the application configuration:
import { Config, IConfigSchema } from '@foal/core';
const configSchema = {
database: {
uri: { type: 'string', required: true }
},
myOtherCustomConfig: {
type: 'boolean|string',
}
} satisfies IConfigSchema;
export const config = Config.load(configSchema);
Then anywhere in the application code, the configuration would be accessible like a regular object:
import { config } from '../relative-path';
// Application configuration
console.log(config.database.uri) // TypeScript type: string
console.log(config.myOtherCustomConfig) // TypeScript type: boolean | string | undefined
// Framework configuration (optional)
console.log(config.settings.social.facebook.clientId) // TypeScript type: string
The properties of the config
object would always be defined. When reading a configuration value, Foal would look at the config schema, fetch the configuration normally (config/*
files, .env
, etc) and validate and convert the value based on the schema. If any, the errors thrown would be the same as those of Config.get
.
Hi, I'm opening a new issue as you asked it in #880 I took a deep dive into the Typescript type system and was able to come up with a type (lot of ideas came from the type-challanges) for the Config.get function:
As you might notice, the solution heavily relies on the default.json, if we want to read a value which is missing from that file, typescript will throw an error. In my opinion this is more of a positive outcome than a negative, as it forces to define a default value for every key that the application will try to read, also making the defaultValue parameter obsolete (defaults should be defined in default.json). Another limitation is in a json file we can only have string, boolean, number, array and object types, the type the function returns with can only be these. So if we want to have a more restricted type (e.g. Stripe API has an Interval type which is a subset of string ("weekly" | "daily" | "manual" | "monthly") and we want to use the value straight from the config) we either need to cast the type, be able to specify the return type of the function similar to the current solution, or have some utility to rewrite the type of the config (which I included in the solution). But in my opinion with these limitations we would get a type-safe, easy-to-use Config functionality. I'm really interested in your opinion (and I don't have a clear idea, how to include this in the framework)
Some screenshots to show how it works: intellisense can show you all the available keys
typescript error if key doesn't exists, value automatically typed correctly
works with deep keys with dot notation, correctly returns with complex object types
I tried to add as many comments as possible, as it is not an easy read :). (it also uses some of the newest features of typescript, I used version 4.7.3)