有没有一种方法可以用基于现有常量的计算字符串在TypeScript中声明联合类型



让我们假设我们有以下常量:

const something = {
foo: {
bar: {
num: 67,
str: 'str',
},
},
some: {
prop: 12,
},
topProp: 25,
};

任务:

实现以下深度属性访问函数的类型检查


/**
* @example
* getByPath('foo/bar/str'); // returns 'str'
* 
* @example
* getByPath('topProp'); // returns 25
* 
* @example
* getByPath('some/prop'); // returns 12
*/
const getByPath = (path: ComputedUnionType) => {<unrelated-code-magic>};
// Where
type ComputedUnionType = 'foo/bar/num' | 'foo/bar/str' | 'some/prop' | 'topProp';
// or even better
type ComputedUnionType<typeof some> = 'foo/bar/num' | 'foo/bar/str' | 'some/prop' | 'topProp';
const getByPath = <T>(path: ComputedUnionType<T>) => ...

我做了什么

  1. 实现了获取有效路径数组的函数,但它返回了简单字符串数组(显然是-_-(,因此找不到任何方法来使用它强制类型
  2. 阅读了一堆关于枚举类型的文章,结果发现枚举类型在这里对我没有帮助,因为它们的属性值只能是计算数字,而不能是字符串(而且可能无论如何都没有帮助,原因是它们的属性本身不能即时生成(
  3. 在实现元组类型检查时偶然发现了这个答案,但在我的案例中未能以某种方式利用它。读起来很有趣,但通常情况下,所提供的解决方案会处理现有的联合类型和键,但从不计算新的类型和键

猜测

  1. 也许它可以是一个递归调用自己的类型,比如deep-partial或类似的类型
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
  1. 也许有一些方法可以通过泛型类型从底层实现它,比如keyof bar、keyof foo等等

当TypeScript 4.1落地时,您将能够通过在microsoft/TypeScript#40336中实现的模板文本类型来操作字符串文本类型。这里有一个可能的实现,可以将类型转换为斜杠分隔的路径的并集,从而导致非对象属性:

type Join<K, P> = K extends string | number ?
P extends string | number ?
`${K}${"" extends P ? "" : "/"}${P}`
: never : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T] : "";

我在这里放了显式递归限制器,所以如果你尝试执行Leaves<SomeTreelikeType>,你可以选择最大深度。如果你不在乎,你可以忘记PrevD,只需要

type Leaves<T> = T extends object ?
{ [K in keyof T]-?: Join<K, Leaves<T[K]>> }[keyof T] : "";

这给了你想要的联盟:

type ComputedUnionType = Leaves<typeof something>;
// type ComputedUnionType = "topProp" | "foo/bar/str" | "foo/bar/num" | "some/prop"

没有问的部分是如何让编译器将路径的类型转换为结果输出的类型。这也是可能的(递归条件类型在#40002中实现,也在TS4.1中实现(,但由于你没有问我,我不会花时间实现它

游乐场链接到代码

还可以添加我的解决方案,它主要相当于jcalz的未检查递归版本,但结合了Join助手。在本例中,它的行为相同,但如果对象具有字符串和数字以外的值,或者对象类型具有可选的?键,则它的行为可能不同。

type ComputedUnionType<T, S extends string = '/'> = {
[K in keyof T]:
K extends string
? ComputedUnionType<T[K]> extends string
? `${K}${S}${ComputedUnionType<T[K]>}`
: K
: never;
}[keyof T];
type Z = ComputedUnionType<typeof something>;
// type Z = "topProp" | "foo/bar/str" | "foo/bar/num" | "some/prop"

游乐场链接

最新更新