我可以使用 TypeScript 从字符串字典/记录中填充强类型“环境”对象吗?

问题描述 投票:0回答:1

我不确定这是否可能(或实用),但我很好奇。


我有一个强类型的

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;

那么...这有可能吗,还是我只是在浪费时间?

reactjs typescript create-react-app
1个回答
0
投票

制作一种包装类来存储原始字符串,然后使用一些 getter 来进行转换,但我希望能够自动执行此操作,而不是为每个值详尽地创建 getter。

IIUC,您有一个与环境配置匹配的

Variables
TypeScript 类型,其中所有值实际上都是字符串(通常是我们从
dotenv
获得的),并且所有键都在 UPPER_SNAKE_CASE 中,并以“REACT_APP_”为前缀。

现在您希望某些实用程序将某些值转换为正确的预期类型(通常将

"true"
字符串转换为
true
布尔值),并且如果可能,将键转换为驼峰式命名法(并删除前缀),同时最大限度地减少对此类实用程序的配置。

在这种情况下,正如您已经发现的那样,不幸的是,没有削减最低配置,因为 TypeScript 类型在运行时不存在,但您确实需要在运行时(当您收到环境变量时)进行转换。

与运行时验证用例类似(参见例如JoiYupZod等),您可以反转该方法:首先将配置编写为结构的“真实来源”您的环境变量,然后从该配置推断后者的类型。

能够将变量名转变为驼峰式命名,并删除“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

© www.soinside.com 2019 - 2024. All rights reserved.