安全地将字符串值映射到对象内的类型

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

Type-safe mapping of string values to types inside an object

问题

以下是翻译好的部分:

有没有一种方法可以将对象的每个字段的类型映射到另一种类型,使用 TypeScript?

更具体地说,我正在尝试构建一个函数,该函数接受某种类型定义,根据每个字段的类型运行不同的函数,并将结果放回到具有相同名称的对象中:

// 函数的输入
registerData({
    bodyText: 'string',
    isAdmin: 'boolean',
    postCount: 'number'
});

// 该函数返回如下:
{
    bodyText: 'Lorem ipsum dolor sit amet',
    isAdmin: true,
    postCount: 20
}

// 因此,所需的类型将是:
{
    bodyText: string,
    isAdmin: bool,
    postCount: number
}

有没有办法获取此返回值的正确类型?

我不一定要求使用此特定的输入格式,但有一种方法来区分相同类型的数据将会很有用(例如,能够为 intfloat 定义不同的函数,它们最终都变成了 number[])。

有必要能够具有任意的输出字段类型,因为我可能需要传递函数或数组,而不仅仅是原始值。

我已经尝试过一些关于 infer 的技巧,但它会合并所有字段的所有类型:

type DataType = 'string' | 'number' | 'boolean';
type DataDefinition = { [name: string]: DataType }
type DataReturn<T> = T extends { [K in keyof T]: infer Item } ? { [K in keyof T]: DataReturnItem<Item> } : never;
type DataReturnItem<T> = 
      T extends 'string' ? string
    : T extends 'number' ? number
    : T extends 'boolean' ? boolean
    : never;

const registerData = <T extends DataDefinition>(data: T) => {
    // 基于字段类型生成数据
    return data as DataReturn<T>;
}

如果我使用上面提到的示例调用 registerData,它生成的类型如下:

{
    bodyText: string | bool | number,
    isAdmin: string | bool | number,
    postCount: string | bool | number
}

此解决方案也非常笨拙,因为它要求我在 DataReturnItem<T> 的 extends 语句中添加另一个情况,这会很快变得混乱。

英文:

Is there a way of mapping the type of each field of an object to another type with TypeScript?

More specifically, I'm trying to build a function that takes in some sort of type definition, runs a different function depending on the type of each field, and puts the result back into an object under the same name:

// Input to function
registerData({
    bodyText: &#39;string&#39;,
    isAdmin: &#39;boolean&#39;,
    postCount: &#39;number&#39;
});

// The function returns this:
{
    bodyText: &#39;Lorem ipsum dolor sit amet&#39;,
    isAdmin: true,
    postCount: 20
}

// So the desired type would be:
{
    bodyText: string,
    isAdmin: bool,
    postCount: number
}

Is there any way to get the correct type for this return value?

I'm not tied to this specific input format, but it would be useful to have some method of differentiating data of the same type (e.g. being able to define a separate function for int and float that both end up as a number[]).

It would be useful to be able to have any arbitrary types as output fields, as I may need to pass out functions or arrays instead of just primitive values.

I've already tried some trickery with infer, but it combines all of the types of all fields:

type DataType = &#39;string&#39; | &#39;number&#39; | &#39;boolean&#39;;
type DataDefinition = { [name: string]: DataType }
type DataReturn&lt;T&gt; = T extends { [K in keyof T]: infer Item } ? { [K in keyof T]: DataReturnItem&lt;Item&gt; } : never;
type DataReturnItem&lt;T&gt; = 
      T extends &#39;string&#39; ? string
    : T extends &#39;number&#39; ? number
    : T extends &#39;boolean&#39; ? boolean
    : never;

const registerData = &lt;T extends DataDefinition&gt;(data: T) =&gt; {
    // Generate data based on field types
    return data as DataReturn&lt;T&gt;;
}

If I call registerData with the example mentioned above, the type it produces is:

{
    bodyText: string | bool | number,
    isAdmin: string | bool | number,
    postCount: string | bool | number
}

This solution is also really awkward, as it requires me to tack on another case to the extends statements for DataReturnItem&lt;T&gt;, which is going to get ugly fairly quickly.

答案1

得分: 1

You'll need to come up with some sort of mapping from string literal types used for property values in the argument to registerData() to the property value types you want to see in the output of registerData(). Luckily TypeScript has an easy way to represent a mapping from string literal types to arbitrary types: an interface.

interface DataReturn {
  string: string;
  boolean: boolean;
  number: number;
  // add entries here if you need it
}

Then you can give registerData() a generic call signature like this:

declare function registerData<T extends Record<keyof T, keyof DataReturn>>(
  data: T
): { [K in keyof T]: DataReturn[T[K]] }

Here data is of type T constrained to have property values matching the keys of the DataReturn interface.

Let's test it out:

const x = registerData({
  bodyText: 'string',
  isAdmin: 'boolean',
  postCount: 'number'
});

Looks good.

Note that it's quite possible to use something like template literal types to encode more complicated relationships from string literals to types, so that, for example, if "XXX" encodes a type XXX, then "Array<XXX>" will be parsed so that it encodes the type XXX[].

But this sort of thing can become arbitrarily complicated, and I'm not about to write an entire type schema parser using template literals. If you want to do it yourself, that's fine, but such endeavors aren't really in scope here.

英文:

You'll need to come up with some sort of mapping from string literal types used for property values in the argument to registerData() to the property value types you want to see in the output of registerData(). Luckily TypeScript has an easy way to represent a mapping from string literal types to arbitrary types: an interface:

interface DataReturn {
  string: string;
  boolean: boolean;
  number: number;
  // add entries here if you need it
}

Then you can give registerData() a generic call signature which like this:

declare function registerData&lt;T extends Record&lt;keyof T, keyof DataReturn&gt;&gt;(
  data: T
): { [K in keyof T]: DataReturn[T[K]] }

Here data is of type T constrained to have property values matching the keys of the DataReturn interface. It's a recursive constraint (aka F-bounded constraint) T extends Record&lt;keyof T, keyof DataReturn&gt;, using the Record&lt;K, V&gt; utility type to say that T must have keys of type keyof T (this is impossible not to happen) and values of type keyof DataReturn (which is the constraint we're trying to enforce). (You could say something like T extends Record&lt;string, keyof DataReturn&gt; but that would possibly reject some inputs, see https://stackoverflow.com/q/55814516/2887218 for more information.)

And the output type is a mapped type where the keys are unchanged but the values are transformed from the keys of DataReturn to their corresponding values.


Let's test it out:

const x = registerData({
  bodyText: &#39;string&#39;,
  isAdmin: &#39;boolean&#39;,
  postCount: &#39;number&#39;
});


/* const x: {
    bodyText: string;
    isAdmin: boolean;
    postCount: number;
} */

Looks good.


Note that it's quite possible to use something like template literal types to encode more complicated relationships from string literals to types, so that, for example, if &quot;XXX&quot; encodes a type XXX, then &quot;Array&lt;XXX&gt;&quot; will be parsed so that it encodes the type XXX[]. Like this, maybe:

type NameToType&lt;K&gt; =
  K extends keyof DataReturn ? DataReturn[K] :
  K extends `Array&lt;${infer K0}&gt;` ? Array&lt;NameToType&lt;K0&gt;&gt; :
  never;

type CheckData&lt;T&gt; = { [K in keyof T]:
  NameToType&lt;T[K]&gt; extends never ? keyof DataReturn : T[K]
}

declare function registerData&lt;const T extends Record&lt;keyof T, string&gt;&gt;(
  data: T extends CheckData&lt;T&gt; ? T : CheckData&lt;T&gt;
): { [K in keyof T]: NameToType&lt;T[K]&gt; }


const x = registerData({
  bodyText: &#39;string&#39;,
  isAdmin: &#39;boolean&#39;,
  postCount: &#39;number&#39;,
  otherStuff: &#39;Array&lt;string&gt;&#39;
});
/* const x: {
  bodyText: string;
  isAdmin: boolean;
  postCount: number;
  otherStuff: string[];
} */

But this sort of thing can become arbitrarily complicated, and I'm not about to write an entire type schema parser using template literals. If you want to do it yourself, that's fine, but such endeavors aren't really in scope here.

Playground link to code

答案2

得分: 0

根据jcalz的回答,我已经提供了以下解决方案:

type TypeMap = {
  string: string;
  boolean: boolean;
  float: number;
  integer: number;
  // add entries here if you need it
}

const generatorMap: { [K in keyof TypeMap]: any } = {
    string: () => [faker.lorem.word(), faker.lorem.paragraph()],
    integer: () => [faker.number.int()],
    boolean: () => [true, false],
    float: () => [faker.number.float()]
}

function registerDataTest<T extends Record<keyof T, keyof TypeMap>>(t: T) {
    const entries = Object.entries(t)
        .map(([name, type]) => [name, generatorMap[type as keyof TypeMap]]);
      
    // Other logic for extracting a single value from each array
    // e.g. data = Object.fromEntries(
    //  entries.map(([name, generated]) => [name, generated[0]]));

    return data as { [K in keyof T]: TypeMap[T[K]] };
}

const x = registerDataTest({
  bodyText: 'string',
  isAdmin: 'boolean',
  postCount: 'float'
});

x将具有正确的类型和正确的输入参数类型检查。

英文:

Based on the respose by jcalz, I've come up with the following solution:

type TypeMap = {
  string: string;
  boolean: boolean;
  float: number;
  integer: number;
  // add entries here if you need it
}

const generatorMap: { [K in keyof TypeMap]: any } = {
    string: () =&gt; [faker.lorem.word(), faker.lorem.paragraph()],
    integer: () =&gt; [faker.number.int()],
    boolean: () =&gt; [true, false],
    float: () =&gt; [faker.number.float()]
}

function registerDataTest&lt;T extends Record&lt;keyof T, keyof TypeMap&gt;&gt;(t: T) {
    const entries = Object.entries(t)
        .map(([name, type]) =&gt; [name, generatorMap[type as keyof TypeMap]]);
      
    // Other logic for extracting a single value from each array
    // e.g. data = Object.fromEntries(
    //  entries.map(([name, generated]) =&gt; [name, generated[0]]));

    return data as { [K in keyof T]: TypeMap[T[K]] };
}

const x = registerDataTest({
  bodyText: &#39;string&#39;,
  isAdmin: &#39;boolean&#39;,
  postCount: &#39;float&#39;
});

x will have correct types and correct type checking on the input parameters as well.

huangapple
  • 本文由 发表于 2023年8月5日 01:53:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/76838189.html
匿名

发表评论

匿名网友

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

确定