英文:
Describe returned object type for a function which takes an array of object keys OR object containg values of string type
问题
I have a function called getParameters
which takes either an array of parameter names or an object. The function's purpose is to load parameter values based on the provided parameter names and return a key-value object where the key is the parameter name and the value is the parameter value as a string.
这是一个名为getParameters
的函数,它接受参数名的数组或对象。该函数的目的是根据提供的参数名加载参数值,并返回一个键值对象,其中键是参数名,值是字符串类型的参数值。
Here's the TypeScript code you provided:
以下是您提供的TypeScript代码:
async function getParameters<T extends Array<string> | RecursiveObject>(
paramNames: T,
): Promise<ParamValuesDictionary> {
const paramNamesArr = !Array.isArray(paramNames)
? toFlatArray(paramNames)
: paramNames as Array<string>;
// Stub implementation of loading parameters
const loadedParams: Parameter[] = paramNamesArr.map(name => ({
name: name,
val: `${name}_val`,
}));
const paramValues = convertToDictionary(loadedParams);
return paramValues;
}
function toFlatArray(obj: RecursiveObject): string[] {
let values: string[] = [];
for (const value of Object.values(obj)) {
if (typeof value === 'object') {
values = values.concat(toFlatArray(value));
} else {
values.push(value);
}
}
return values;
}
function convertToDictionary(
parameters: Parameter[],
): ParamValuesDictionary {
const paramValues: ParamValuesDictionary = parameters.reduce(
(acc, { name, val }) => {
acc[name] = val;
return acc;
},
{} as ParamValuesDictionary,
);
return paramValues;
}
type Parameter = {
name: string;
val: string;
};
type RecursiveObject = {
[key: string]: string | RecursiveObject;
};
type ParamValuesDictionary = { [name: string]: string };
Right now, the result of the getParameters
function is a ParamValuesDictionary
type, which allows any string as a key. If you want to define strict keys in ParamValuesDictionary
based on the provided paramNames
function argument, you can modify the function like this:
现在,getParameters
函数的结果是ParamValuesDictionary
类型,它允许任何字符串作为键。如果您想根据提供的paramNames
函数参数定义ParamValuesDictionary
中的严格键,可以像这样修改函数:
async function getParameters<T extends Array<string> | RecursiveObject>(
paramNames: T,
): Promise<StrictParamValuesDictionary> {
const paramNamesArr = !Array.isArray(paramNames)
? toFlatArray(paramNames)
: paramNames as Array<string>;
// Stub implementation of loading parameters
const loadedParams: Parameter[] = paramNamesArr.map(name => ({
name: name,
val: `${name}_val`,
}));
const paramValues = convertToDictionary(loadedParams);
return paramValues;
}
type StrictParamValuesDictionary = { [K in string]: string };
// Rest of the code remains the same.
In this modified version, StrictParamValuesDictionary
restricts the keys to be of type string
.
英文:
I have a function called getParameters
which takes either array of parameter names, or object. The aim of the function is to load parameter values by provided parameter names. It returns a key-value object where key is parameter name and value - parameter value of string type.
These are how the function and the example of usage look like:
async function getParameters<T extends Array<string> | RecursiveObject>(
paramNames: T,
): Promise<ParamValuesDictionary> {
const paramNamesArr = !Array.isArray(paramNames)
? toFlatArray(paramNames)
: paramNames as Array<string>;
// Stub implementation of loading parameters
const loadedParams: Parameter[] = paramNamesArr.map(name => ({
name: name,
val: `${name}_val`,
}));
const paramValues = convertToDictionary(loadedParams);
return paramValues;
}
function toFlatArray(obj: RecursiveObject): string[] {
let values: string[] = [];
for (const value of Object.values(obj)) {
if (typeof value === 'object') {
values = values.concat(toFlatArray(value));
} else {
values.push(value);
}
}
return values;
}
function convertToDictionary(
parameters: Parameter[],
): ParamValuesDictionary {
const paramValues: ParamValuesDictionary = parameters.reduce(
(acc, { name, val }) => {
acc[name] = val;
return acc;
},
{} as ParamValuesDictionary,
);
return paramValues;
}
type Parameter = {
name: string;
val: string;
};
type RecursiveObject = {
[key: string]: string | RecursiveObject;
};
type ParamValuesDictionary = { [name: string]: string };
getParameters(['a', 'b']).then(parameters => {
console.log('Passed array:', parameters);
/* OUTPUT:
"Passed array:", {
"a": "a_val",
"b": "b_val"
}
*/
});
getParameters({
nameA: 'a',
nameB: 'b',
namesC: {
nameC1: 'c1',
nameC2: 'c2',
},
}).then(parameters => {
console.log('Passed object:', parameters);
/* OUTPUT:
"Passed object:", {
"a": "a_val",
"b": "b_val",
"c1": "c1_val",
"c2": "c2_val"
}
*/
});
Right now the result of getParameters
function is ParamValuesDictionary
, which allows any string as a key. I would like to define strict keys of ParamValuesDictionary
based on provided paramNames
function argument. How should i change the type ParamValuesDictionary
to apply this behavior? Appreciate any help.
答案1
得分: 1
以下是您要翻译的内容:
当您调用getParameters(parameters)
时,其中parameters
是通用类型T
受限 为要么是RecursiveObject
要么是string
数组,您希望返回的值是一种对象类型,其值都为string
,键由T
以特定方式确定。让我们称这个操作为Keys<T>
。因此,getParameters()
的调用签名应如下所示:
declare function getParameters<const T extends readonly string[] | RecursiveObject>(
paramNames: T,
): Promise<{ [K in Keys<T>]: string }>;
请注意,我在T
上使用了const类型参数修改器;这要求编译器从getParameters(⋯)
中推断T
,就好像调用已经是getParameters(⋯ as const)
并带有const
断言。否则,像{k1: "v1"}
这样的参数值将被推断为类型{k1: string}
,这对您的需求不够具体。您希望跟踪属性值的字面类型,就好像编写了{k1: "v1"}
as const,从而得到类型{readonly k1: "v1"}
。
因此,我已经将string[]
更改为readonly string[]
。ReadonlyArray
类型比类似的Array
类型更宽松,而且由于const
参数将为数组文字推断readonly
元组,我们需要接受更宽泛的类型。
所以现在我们需要定义Keys<T>
:
如果T
是RecursiveObject
的某个子类型,那么Keys<T>
应该是其叶子节点的字符串文字类型的联合。因此,我们需要一些递归实用类型RecursiveObjectLeaves<T>
来计算它。否则,T
将是一个字符串文字类型元素的数组,我们只需通过索引在T
中使用number
进行联合。所以Keys<T>
看起来像这样:
type Keys<T extends readonly string[] | RecursiveObject> =
T extends RecursiveObject ? RecursiveObjectLeaves<T> :
T extends readonly string[] ? T[number] : never
现在我们需要定义RecursiveObjectLeaves<T>
。
这是计算它的一种方法:
type RecursiveObjectLeaves<T> =
T extends RecursiveObject ?
{ [K in keyof T]: RecursiveObjectLeaves<T[K]> }[keyof T]
: Extract<T, string>
这是一个条件类型,它检查T
是否为对象;如果是,则我们递归调用RecursiveObjectLeaves
来处理每个属性,并将联合作为分布式对象类型生成(如ms/TS#47109中所定义),然后我们立即索引到一个映射类型。如果不是,则T
是叶子节点,我们只需返回它... 或者更确切地说是Extract<T, string>
,使用Extract实用类型来说服编译器这将是一个string
。
让我们进行测试:
type ROLTest = RecursiveObjectLeaves<{
k1: "v1",
k2: {
k3: "v3",
k4: "v4",
k5: { k6: { k7: { k8: "v8" } } }
}
}>;
// type ROLTest = "v1" | "v3" | "v4" | "v8"
看起来不错。
现在我们可以把所有这些放在一起(我只会在getParameters()
的实现内使用类型断言,以避免编译器错误。编译器永远不会理解它返回的值实际上是泛型T
的Promise<{ [K in Keys<T>]: string }>
类型;这超出了其分析能力的范围。所以它看起来像这样:
async function getParameters<const T extends readonly string[] | RecursiveObject>(
paramNames: T,
): Promise<{ [K in Keys<T>]: string }> {
⋯
return paramValues as any;
}
我们可以测试它的行为:
getParameters(['a', 'b']).
<details>
<summary>英文:</summary>
When you call `getParameters(parameters)` where `parameters` is of [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) type `T` [constrained](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints) to be either a `RecursiveObject` or an array of `string`s, you want the value returned to be of an object types whose values are all `string` and whose keys are determined by `T` in a particular way. Let's call that operation `Keys<T>`. So `getParameters()`'s call signature should be
declare function getParameters<const T extends readonly string[] | RecursiveObject>(
paramNames: T,
): Promise<{ [K in Keys<T>]: string }>;
Notice that I'm using [a `const` type parameter modifier](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters) on `T`; this asks the compiler to infer `T` from `getParameters(⋯)` as if the call had been `getParameters(⋯ as const)` with a [`const` assertion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions). Otherwise a `parameters` value like `{k1: "v1"}` will be inferred as type `{k1: string}`, and that's not specific enough for your needs. You want to keep track of the [literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) of the property values, as if you had written `{k1: "v1"} as const`, resulting in the type `{readonly k1: "v1"}`.
And therefore I've changed `string[]` to `readonly string[]`. The [`ReadonlyArray`](https://www.typescriptlang.org/docs/handbook/2/objects.html#the-readonlyarray-type) type is less restrictive than the analogous [`Array`](https://www.typescriptlang.org/docs/handbook/2/objects.html#the-array-type) type, and since a `const` parameter will infer [`readonly` tuples](https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-tuple-types) for array literals, we will need to accept the wider type.
So now we have to define `Keys<T>`:
----
If `T` is some subtype of `RecursiveObject`, then `Keys<T>` should be the [union](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) of the string literal types of its leaf nodes. So we'll need some recursive utility type `RecursiveObjectLeaves<T>` to compute that. Otherwise `T` will be an array of string literal type elements, and we just want to grab the union of those elements by [indexing](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) into `T` with `number`. So `Keys<T>` will look like:
type Keys<T extends readonly string[] | RecursiveObject> =
T extends RecursiveObject ? RecursiveObjectLeaves<T> :
T extends readonly string[] ? T[number] : never
So now we have to define `RecursiveObjectLeaves<T>`.
---
Here's one way to compute it:
type RecursiveObjectLeaves<T> =
T extends RecursiveObject ?
{ [K in keyof T]: RecursiveObjectLeaves<T[K]> }[keyof T]
: Extract<T, string>
That's a [conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) that checks if `T` is an object; if so then we recursively call `RecursiveObjectLeaves` on each property and produce the union as a *distributive object type* (as coined in [ms/TS#47109](https://github.com/microsoft/TypeScript/pull/47109)) where we immediately [index into](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) a [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html). If not then `T` is a leaf node and we just return it... or rather `Extract<T, string>` using [the `Extract` utility type](https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union) to convince the compiler that this will be a `string`.
Let's test that:
type ROLTest = RecursiveObjectLeaves<{
k1: "v1",
k2: {
k3: "v3",
k4: "v4",
k5: { k6: { k7: { k8: "v8" } } }
}
}>;
// type ROLTest = "v1" | "v3" | "v4" | "v8"
Looks good.
----
So now we can put it all together (I'm just going to use [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) inside the *implementation* of `getParameters()` to avoid compiler errors. There's no way the compiler will ever understand that the value it returns will actually be of the type `Promise<{ [K in Keys<T>]: string }>` for generic `T`; it's just beyond the scope of its abilities to analyze. So it will look like
async function getParameters<const T extends readonly string[] | RecursiveObject>(
paramNames: T,
): Promise<{ [K in Keys<T>]: string }> {
⋯
return paramValues as any;
}
And we can test how it behaves:
getParameters(['a', 'b']).
then(parameters => {
// ^? (parameter) parameters: { a: string; b: string; }
console.log('Passed array:', parameters);
});
getParameters({
nameA: 'a',
nameB: 'b',
namesC: {
nameC1: 'c1',
nameC2: 'c2',
},
}).then(parameters => {
// ^? (parameter) parameters:
// { a: string; b: string; c1: string; c2: string; }
console.log('Passed object:', parameters);
});
Looks good. In the array case the compiler retrieves the literal types of `"a"` and `"b"` and uses them as the keys of the promised return value. In the object case the compiler retrieves the literal types of `"a"`, `"b"`, `"c1"` and `"c2"` at the leaves of the nested property tree and uses them as the keys of the promised return value.
[Playground link to code](https://www.typescriptlang.org/play?#code/C4TwDgpgBAShDGBXATgZwJYDcIHkBGAVgsADIQCG2qAPACoB8UAvAFBRS1QQAewEAdgBNUsBCgzZ8ReMCgB+NlADeUANoBpKOn5QA1hBAB7AGYcAugC5RSNFlyFiZShBq0NZxgF9V+o6dpmilYAorzI5DJ0ADRQqMDI2gDm9CwsoJCwOCS0LrJM1uJ2Uo4UVNRKiroAjFYARJhVtVGVAExWFezsugDMdZjdTYpdACx9w4OdegCs7XoAbLO6AOyLABx9q7VQntvbip4snvQA3CwA9GdQ6dAwWTlxzFD1jVAAPk-9W+-1428fm6lrlB1AZXFxeAJhFBkBRBIZ+AAbECxeJJVRmP5wGwSezSYCMVjsTg8PhCERYwqSBwyeQFWxUvFOMoMKAWRTEiFk6Gw+FIlEJfiJdG0tz8RAAWzwEGQGKs-Ag2GQqTOACooCxBAgEeQYVBjIh+DJ0PCoIkIMAAAo68ji83SmjweEPDmkqEw8hwxHIuICoUY94U+m44j0AAUijA1vFADkbS4rLRmgBKKwW5CGcXoVAQcpqTTaYGguj0Sz8pLbE4sFVnVLkVAgQ16g1Gk1my1Ru1oaiO-jO8GukTuz18n1o-10nHFGRhiNR2O21AJ5Op9OZ7O5jRaHQgkCuEtWUeCivKVLsHsPSPhGNx1AAQWQyEeAEJ7+EQAA6LOv8ggUOXm3zi4SZDLSwCGAAYtqwDfr+-7XguUB1hORTUsAwGTFYcGASISEwdQh7JKcijnrICKGB6ECCFaV6LlA1FxnwyDCvkWE3q+77iuQYChvwcbMIwoYdJ0vG2nKcbNJMmDkAiVgAAYACRKCJECeAA+lJCKyRJ2xJkmRFnk6shwQAatJiAuI8PaKsAtCGAAIugLa8cgv5kRRVFRqgemKDCwAoDoJlmRZSHkPwICnAcLD6oawDGjoYGQeQ0EPj+oaGIQViBpOqEptyHq8t6qKCsKQkIuaUAaeZtEEcxahmKc7DGIYj6hiRFVBVAJhQFOwDvpVLhpYQuknpM6CmKG1xdf1zBMPkADk6V4nNSYjZM7UIlVjz9ag749vASUTRBUEwaG-W6Q1nQ7BACLZqtklBTtYCIKgAAWp1Bd5kwHOw33cn5yA6NtEWpNFTlQFZ0o2fZjmxfCOq-rOV6drR9G2ox6LLnRUamRtLgOU58OrW1gW4yj2MPfjsPOciLEdoxO0woIiDwBA4aTKGETwDEKjKTEGk6fxd2dJzqjKRi+QaRdky+f5iHwPAUvbNp7BKDsSGozjVWU3F8PaZ9f2yyTVXA2k4DQKjnaPEJykHkViQXRptu+hFpxAllKF4lbig+AYTtJKWBGYmIQY9S7psZBrFMwzrLlW2oNtloKAd29sRFthb9Ohqoc3kHNMRzXgc1mEm76KMAL0CH+dP2oLQnsBcUAAHpyFAVdI4xK1wcjszkH7grHFAeB9-beyTOehhle+ZGJKGc1Wqg2aCIhKUgBYedQF39P6543ksOn1doIJijKbeVg53nx9xgAQmfhfacpqAAMLtCBymPzUUBzfAVQX5Mb9tJ-eALRf5K0OCXculdN41yYIwISDdOjN1blA5AncD60UUPAtayhELDwHkPROI9v64PBgAgiA9frj0ntPWe89F6dVQmvGIyCvIRW8kAA)
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论