常量大小写字符串到对象的类型安全转换

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

有一个名为 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.
}

我想解决方案将涉及打字稿的一些巧妙用法“模板文字类型”

typescript
1个回答
0
投票

我处理此问题的方法是采用像

{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

Playground 代码链接

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