我不确定这是否可能(或实用),但我很好奇。
我有一个强类型的
string
变量字典,如下所示:
const variables: Variables = {
REACT_APP_MY_VARIABLE: 'Test',
REACT_APP_MY_BOOLEAN_VARIABLE: 'true'
};
我直接通过
variables
对象访问这些变量:const myVariable = variables.REACT_APP_MY_VARIABLE;
等。
“问题”是,对于其中一些值,
string
不是值的合理表示(例如'true'
),所以我必须在使用它之前以某种方式转换它。
const myBoolean = toBoolean(variables.REACT_APP_MY_BOOLEAN_VARIABLE);
这基本上很好......但是如果可以将转换后的值存储在一个对象中,并且在未指定转换的情况下,与默认存储为字符串的所有其他值一起存储,那就太好了。
我想过制作一种包装类来存储原始字符串,然后使用一些 getter 来进行转换,但我希望能够自动执行此操作,而不是为每个值详尽地创建 getter。
class EnvironmentVariables {
// This should actually be a private variable but my keyboard
// doesn't have a hash key.
private _variables: Variables;
constructor(variables: Variables) {
this._variables = variables;
}
get myBooleanVariable() {
return toBoolean(this._variables.REACT_APP_MY_BOOLEAN_VARIABLE);
}
}
现在我可以很好地得到我的布尔值了:
const environmentVariables = new EnvironmentVariables(variables);
// myBoolean = true
const myBoolean = environmentVariables.myBooleanVariable;
如果我想要其他的,我必须为每个都创建一个 getter,当最常见的情况只是返回
string
值时,这需要付出很大的努力:
// I don't want to make one of these for every value.
get myVariable() {
return this._variables.REACT_APP_MY_VARIABLE;
}
为此,我可以以编程方式为任何值添加“默认”getter,而无需显式覆盖,并将其映射到内部
_variables
集合。
constructor(variables: Variables) {
this._variables = variables;
for (const key in variables) {
const getterName = key.replace('REACT_APP_', '').toCamelCase();
if (this.hasOwnProperty(getterName)) {
continue;
}
Object.defineProperty(this, getterName, {
get: () => this._variables[key];
});
}
}
这个效果太棒了!除了我不知道如何让 TypeScript 识别出当我向
variables
添加值时,该值也应该可以通过 EnvironmentVariables
访问。
const environmentVariables = new EnvironmentVariables(variables);
// myBooleanVariable = true, I am happy.
const myBooleanVariable = environmentVariables.myBooleanVariable;
// myVariable = 'Test'... but TypeScript gives an error saying there's
// no 'myVariable' on type 'EnvironmentVariables'.
const myVariable = environmentVariables.myVariable;
我也非常希望它能够将变量名称转变为驼峰式大小写,并删除“REACT_APP”前缀,因为它非常笨重。我怀疑这是否可能,但我想也许这可以通过模板类型和泛型来完成?
本质上,我想要像这样工作的东西:
const variables = {
REACT_APP_MY_VARIABLE: 'Test',
REACT_APP_MY_BOOLEAN_VARIABLE: 'true'
};
class EnvironmentVariables {
... // Magic goes here.
get myBooleanVariable() {
return toBoolean(this._variables.REACT_APP_MY_BOOLEAN_VARIABLE);
}
}
const environmentVariables = new EnvironmentVariables(variables);
// TypeScript allows this even though there's no getter defined in
// EnvironmentVariables, and it recognises that the value is a `string`.
const myVariable = environmentVariables.myVariable;
// There's a getter defined for myBooleanVariable, so TypeScript
// recognises that it is a boolean.
const myBooleanVariable = environmentVariables.myBooleanVariable;
那么...这有可能吗,还是我只是在浪费时间?
制作一种包装类来存储原始字符串,然后使用一些 getter 来进行转换,但我希望能够自动执行此操作,而不是为每个值详尽地创建 getter。
IIUC,您有一个与环境配置匹配的
Variables
TypeScript 类型,其中所有值实际上都是字符串(通常是我们从 dotenv
获得的),并且所有键都在 UPPER_SNAKE_CASE 中,并以“REACT_APP_”为前缀。
现在您希望某些实用程序将某些值转换为正确的预期类型(通常将
"true"
字符串转换为 true
布尔值),并且如果可能,将键转换为驼峰式命名法(并删除前缀),同时最大限度地减少对此类实用程序的配置。
在这种情况下,正如您已经发现的那样,不幸的是,没有削减最低配置,因为 TypeScript 类型在运行时不存在,但您确实需要在运行时(当您收到环境变量时)进行转换。
与运行时验证用例类似(参见例如Joi、Yup、Zod等),您可以反转该方法:首先将配置编写为结构的“真实来源”您的环境变量,然后从该配置推断后者的类型。
能够将变量名转变为驼峰式命名,并删除“REACT_APP”前缀[...]也许这可以通过模板类型和泛型来完成?
事实上,TypeScript 现在能够对类型执行一些有限的字符串操作。内置实用程序不处理蛇形命名法到驼峰命名法的转换(也不处理相反的转换),但有几种可用的解决方案,例如在 是否可以在 Typescript 中使用映射类型来更改类型键名称?
// Atomic type converters used for configuration
function envString(envValue: string) {
return envValue;
}
function envBoolean(envValue: "true" | "false") {
return envValue === "true";
}
// Example configuration
const config = {
// Use camelCase
myVariable: envString,
myBooleanVariable: envBoolean,
}
// Example generating the converterFn
const myEnvConverter = envConverterFnBuilder(config);
const result = myEnvConverter({
REACT_APP_MY_VARIABLE: 'Test',
REACT_APP_MY_BOOLEAN_VARIABLE: 'true'
}); // Okay
result.myBooleanVariable;
// ^? (property) myBooleanVariable: boolean
上面的
envConverterFnBuilder
ConverterFn构建器,它使用了一些辅助类型,包括提到的CamelToSnake
解决方案:
// ConverterFn builder
function envConverterFnBuilder<T extends { [Key in keyof T]: typeof envString | typeof envBoolean }>(envStructure: T) {
return function envConverterFn(envDict: ConverterInput<T>): ConverterOutput<T> {
const result = {} as any; // Will be populated below
for (const configKey in envStructure) {
// Convert camelCase key into UPPER_SNAKE_CASE with prefix, e.g. using Lodash
const ENV_KEY = _.toUpper(_.snakeCase("reactApp_" + configKey)) as keyof ConverterInput<T>;
const atomicConverter = envStructure[configKey];
result[configKey] = atomicConverter(envDict[ENV_KEY] as any); // We know that the envValue matches the converter because they derive from the same key
}
return result;
};
}
// Mapped types with key remapping using template literal type manipulation
type ConverterInput<T extends { [Key in keyof T]: typeof envString | typeof envBoolean }> = {
[Key in keyof T as Uppercase<CamelToSnake<`reactApp_${string & Key}`>>]: Parameters<T[Key]>[0]
};
type ConverterOutput<T extends { [Key in keyof T]: typeof envString | typeof envBoolean }> = {
[Key in keyof T]: ReturnType<T[Key]>
}
现场演示(检查运行时行为):https://playcode.io/1751585