Describe returned object type for a function which takes an array of object keys OR object containg values of string type

huangapple go评论50阅读模式
英文:

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&lt;T extends Array&lt;string&gt; | RecursiveObject&gt;(
paramNames: T,
): Promise&lt;ParamValuesDictionary&gt; {
const paramNamesArr = !Array.isArray(paramNames)
? toFlatArray(paramNames)
: paramNames as Array&lt;string&gt;;
// Stub implementation of loading parameters
const loadedParams: Parameter[] = paramNamesArr.map(name =&gt; ({
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 === &#39;object&#39;) {
values = values.concat(toFlatArray(value));
} else {
values.push(value);
}
}
return values;
}
function convertToDictionary(
parameters: Parameter[],
): ParamValuesDictionary {
const paramValues: ParamValuesDictionary = parameters.reduce(
(acc, { name, val }) =&gt; {
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([&#39;a&#39;, &#39;b&#39;]).then(parameters =&gt; {
console.log(&#39;Passed array:&#39;, parameters);
/* OUTPUT:
&quot;Passed array:&quot;,  {
&quot;a&quot;: &quot;a_val&quot;,
&quot;b&quot;: &quot;b_val&quot;
} 
*/
});
getParameters({
nameA: &#39;a&#39;,
nameB: &#39;b&#39;,
namesC: {
nameC1: &#39;c1&#39;,
nameC2: &#39;c2&#39;,
},
}).then(parameters =&gt; {
console.log(&#39;Passed object:&#39;, parameters);
/* OUTPUT:
&quot;Passed object:&quot;,  {
&quot;a&quot;: &quot;a_val&quot;,
&quot;b&quot;: &quot;b_val&quot;,
&quot;c1&quot;: &quot;c1_val&quot;,
&quot;c2&quot;: &quot;c2_val&quot;
} 
*/
});

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.

TS Playground link is available here.

答案1

得分: 1

以下是您要翻译的内容:

当您调用getParameters(parameters)时,其中parameters通用类型T 受限 为要么是RecursiveObject要么是string数组,您希望返回的值是一种对象类型,其值都为string,键由T以特定方式确定。让我们称这个操作为Keys&lt;T&gt;。因此,getParameters()的调用签名应如下所示:

declare function getParameters&lt;const T extends readonly string[] | RecursiveObject&gt;(
  paramNames: T,
): Promise&lt;{ [K in Keys&lt;T&gt;]: string }&gt;;

请注意,我在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&lt;T&gt;

如果TRecursiveObject的某个子类型,那么Keys&lt;T&gt;应该是其叶子节点的字符串文字类型的联合。因此,我们需要一些递归实用类型RecursiveObjectLeaves&lt;T&gt;来计算它。否则,T将是一个字符串文字类型元素的数组,我们只需通过索引T中使用number进行联合。所以Keys&lt;T&gt;看起来像这样:

type Keys&lt;T extends readonly string[] | RecursiveObject&gt; =
  T extends RecursiveObject ? RecursiveObjectLeaves&lt;T&gt; :
  T extends readonly string[] ? T[number] : never

现在我们需要定义RecursiveObjectLeaves&lt;T&gt;

这是计算它的一种方法:

type RecursiveObjectLeaves&lt;T&gt; =
  T extends RecursiveObject ?
  { [K in keyof T]: RecursiveObjectLeaves&lt;T[K]&gt; }[keyof T]
  : Extract&lt;T, string&gt;

这是一个条件类型,它检查T是否为对象;如果是,则我们递归调用RecursiveObjectLeaves来处理每个属性,并将联合作为分布式对象类型生成(如ms/TS#47109中所定义),然后我们立即索引到一个映射类型。如果不是,则T是叶子节点,我们只需返回它... 或者更确切地说是Extract&lt;T, string&gt;,使用Extract实用类型来说服编译器这将是一个string

让我们进行测试:

type ROLTest = RecursiveObjectLeaves&lt;{
  k1: &quot;v1&quot;,
  k2: {
    k3: &quot;v3&quot;,
    k4: &quot;v4&quot;,
    k5: { k6: { k7: { k8: &quot;v8&quot; } } }
  }
}>;
// type ROLTest = &quot;v1&quot; | &quot;v3&quot; | &quot;v4&quot; | &quot;v8&quot;

看起来不错。

现在我们可以把所有这些放在一起(我只会在getParameters()实现内使用类型断言,以避免编译器错误。编译器永远不会理解它返回的值实际上是泛型TPromise&lt;{ [K in Keys&lt;T&gt;]: string }&gt;类型;这超出了其分析能力的范围。所以它看起来像这样:

async function getParameters&lt;const T extends readonly string[] | RecursiveObject&gt;(
  paramNames: T,
): Promise&lt;{ [K in Keys&lt;T&gt;]: string }&gt; {

  

  return paramValues as any;
}

我们可以测试它的行为:

getParameters([&#39;a&#39;, &#39;b&#39;]).


<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&#39;s call that operation `Keys&lt;T&gt;`.  So `getParameters()`&#39;s call signature should be

    declare function getParameters&lt;const T extends readonly string[] | RecursiveObject&gt;(
      paramNames: T,
    ): Promise&lt;{ [K in Keys&lt;T&gt;]: string }&gt;;

Notice that I&#39;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: &quot;v1&quot;}` will be inferred as type `{k1: string}`, and that&#39;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: &quot;v1&quot;} as const`, resulting in the type `{readonly k1: &quot;v1&quot;}`.  

And therefore I&#39;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&lt;T&gt;`:

----

If `T` is some subtype of `RecursiveObject`, then `Keys&lt;T&gt;` 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&#39;ll need some recursive utility type `RecursiveObjectLeaves&lt;T&gt;` 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&lt;T&gt;` will look like:

    type Keys&lt;T extends readonly string[] | RecursiveObject&gt; =
      T extends RecursiveObject ? RecursiveObjectLeaves&lt;T&gt; :
      T extends readonly string[] ? T[number] : never

So now we have to define `RecursiveObjectLeaves&lt;T&gt;`.

---

Here&#39;s one way to compute it:

    type RecursiveObjectLeaves&lt;T&gt; =
      T extends RecursiveObject ?
      { [K in keyof T]: RecursiveObjectLeaves&lt;T[K]&gt; }[keyof T]
      : Extract&lt;T, string&gt;

That&#39;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&lt;T, string&gt;` 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&#39;s test that:

    type ROLTest = RecursiveObjectLeaves&lt;{
      k1: &quot;v1&quot;,
      k2: {
        k3: &quot;v3&quot;,
        k4: &quot;v4&quot;,
        k5: { k6: { k7: { k8: &quot;v8&quot; } } }
      }
    }&gt;;
    // type ROLTest = &quot;v1&quot; | &quot;v3&quot; | &quot;v4&quot; | &quot;v8&quot;

Looks good.

----

So now we can put it all together (I&#39;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&#39;s no way the compiler will ever understand that the value it returns will actually be of the type `Promise&lt;{ [K in Keys&lt;T&gt;]: string }&gt;` for generic `T`; it&#39;s just beyond the scope of its abilities to analyze.   So it will look like

    async function getParameters&lt;const T extends readonly string[] | RecursiveObject&gt;(
      paramNames: T,
    ): Promise&lt;{ [K in Keys&lt;T&gt;]: string }&gt; {
    
      

      return paramValues as any;
    }

And we can test how it behaves:

    getParameters([&#39;a&#39;, &#39;b&#39;]).
      then(parameters =&gt; {
        // ^? (parameter) parameters: { a: string; b: string; }
        console.log(&#39;Passed array:&#39;, parameters);
      });
    
    getParameters({
      nameA: &#39;a&#39;,
      nameB: &#39;b&#39;,
      namesC: {
        nameC1: &#39;c1&#39;,
        nameC2: &#39;c2&#39;,
      },
    }).then(parameters =&gt; {
      //    ^? (parameter) parameters: 
      //        { a: string; b: string; c1: string; c2: string; }
      console.log(&#39;Passed object:&#39;, parameters);
    });

Looks good.  In the array case the compiler retrieves the literal types of `&quot;a&quot;` and `&quot;b&quot;` and uses them as the keys of the promised return value.  In the object case the compiler retrieves the literal types of `&quot;a&quot;`, `&quot;b&quot;`, `&quot;c1&quot;` and `&quot;c2&quot;` 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>



huangapple
  • 本文由 发表于 2023年5月29日 20:37:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76357444.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定