英文:
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>
				通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论