有一个名为 nconf 的配置库,它的一个功能是获取环境变量并将它们转换为对象:
MYFOO_BAR_BAZ=true MYFOO_BAR_COUNT=34 MYFOO_REALLY_KABOOM=9000 tsx ./main.ts
// main.ts
import { options } from './options.ts';
console.log(options);
// options.ts
import * as nconf from 'nconf';
import home from 'user-home';
const KEY = 'MYFOO';
const ENV_SEPARATOR = `__`;
const ENV_PATTERN = new RegExp(`/^${KEY.toUpperCase()}${ENV_SEPARATOR}/`);
const RC_NAME = `.${KEY}rc.json`;
nconf.env({
separator: ENV_SEPARATOR,
match: ENV_PATTERN,
lowerCase: true,
parseValues: true,
transform(obj: Record<string, string>) {
obj.key.replace(ENV_PATTERN, '');
return obj;
},
});
export const options = nconf.get()
用这个运行你最终会得到一个对象:
{
bar: {
baz: true,
count: 34,
},
really: {
kaboom: 9000
}
}
现在,我还有一个描述环境变量的模式:
type Config = {
BAR_BAZ: boolean;
BAR_COUNT: number;
REALLY_KABOOM: number;
}
这里的问题是:如何根据我拥有的模式使 nconf 输出类型安全?
import { options } from './options.ts';
function somethingElseUseful(yesno: boolean) {
...something
}
export function usefulFunction() {
return somethingElseUseful(options.bar.count) // should throw type error that it only accepts boolean and not numbers.
}
我想解决方案将涉及打字稿的一些巧妙用法“模板文字类型”
我处理此问题的方法是采用像
{A_B_C: X, D_E_F: Y, G_H_I: Z}
这样的类型,并将每个键设置为小写,例如 {a_b_c: X, d_e_f: Y, g_h_i: Z}
,然后对于每个属性,计算“深度记录”类型并将它们全部相交,就像 {a:{b:{c: X}}} & {d:{e:{f: Y}}} & {g:{h:{i: Z}}}
,最后将这些交叉点折叠成单一类型。让我们研究其中的每一部分:
为了将
"bar_baz"
和 boolean
等条目转换为 {bar: {baz: boolean}}
,您可以编写一个 递归条件 DeepRecord<K, V>
类型:
type DeepRecord<K extends string, V> = K extends `${infer K0}_${infer KR}` ?
Record<K0, DeepRecord<KR, V>> : Record<K, V>;
使用模板文字类型在下划线字符处分割键
K
。所以 DeepRecord<"a_b_c", X>
将是 {a: {b: {c: X}}}
。
将对象类型的交集组合成单个对象类型涉及递归计算“身份”映射类型:
type ExpandRecursively<T> = T extends object ?
{ [K in keyof T]: ExpandRecursively<T[K]> } : T;
这与如何查看 Typescript 类型的完整扩展契约?中的定义类似),并且可以将每个条目转换为交集,如下所示
然后我们可以像这样把它们放在一起:
type Transform<T extends object> =
{ [K in string & keyof T]: (x: DeepRecord<Lowercase<K>, T[K]>) => void } extends
Record<string, (x: infer I) => void> ? ExpandRecursively<I> : never;
我们映射
string
的 keyof T
键,然后对于每个键 K
,我们使用 Lowercase<T>
实用程序类型 将每个键转换为小写,然后再计算深度记录。一旦我们有了深度记录,我们就把它放在逆变类型位置作为函数的参数,这样当我们从它推断到一个新的类型变量I
时,它就会被推断为交集,如上所述在将并集类型转换为交集类型。最后我们 ExpandRecursively
I
键入将其组合起来。
是的,这是一种很多类型的杂耍,但这就是你必须做的才能让它发挥作用。
让我们测试一下:
type Config = {
BAR_BAZ: boolean;
BAR_COUNT: number;
REALLY_KABOOM: number;
}
type Z = Transform<Config>;
/* type Z = {
bar: {
baz: boolean;
count: number;
};
really: {
kaboom: number;
};
} */
看起来不错。请注意,像
Transform
这样的深度递归类型往往具有奇怪的边缘,修改它们有时可能需要进行大量的重构。编写此类型是为了处理问题中的测试用例。您可能会发现某些用例无法正确处理,如果是这样,请做好调整甚至完全重写的准备Transform
。