将函数参数中数组中的类型提取为通用的交集类型。

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

Extract types from array in function argument into a generic intersection type

问题

如何确保T具有以下类型:

type CombinedFields = {
  field1: string,
  field2: string,
} & {
  field3: string,
  field4: string
}

这可以通过以下方式来实现:首先,你可以使用联合类型和泛型参数来提取数组中对象的字段类型。然后,你可以使用交叉类型将这些字段类型组合在一起。以下是示例代码:

function combineFields<T extends Extension[]>(extensions: T): T[number]['fields'] {
  const combinedFields: any = {};

  extensions.forEach(extension => {
    for (const key in extension.fields) {
      if (extension.fields.hasOwnProperty(key)) {
        combinedFields[key] = extension.fields[key];
      }
    }
  });

  return combinedFields as T[number]['fields'];
}

const combinedFields = combineFields(config.extensions);

这将返回一个类型为CombinedFields的对象,包含了所有扩展的字段。

英文:

I'm trying to extract a generic type that is a union of the types of objects present in an array passed within the argument of a function. It's easiest to illustrate this with code, so given the following:

type Extension = {
  name: string,
  fields: {
    [key: string]: string
  }
}

type Config = {
  name: string,
  extensions: Extension[]
}

const ext1: Extension = {
  name: &#39;ext1&#39;,
  fields: {
    field1: &#39;test&#39;,
    field2: &#39;test&#39;
  }
}

const ext2: Extension = {
  name: &#39;ext1&#39;,
  fields: {
    field3: &#39;test&#39;,
    field4: &#39;test&#39;
  }
}

const config: Config = {
  name: &#39;Test&#39;,
  extensions: [ext1, ext2]
}

// Ignore the obviously wrong typings here
function create(config: Config): T { ... }

How can I ensure that T has the following type:

type CombinedFields = {
  field1: string,
  field2: string,
} &amp; {
  field3: string,
  field4: string
}

答案1

得分: 1

以下是翻译好的部分:

In order for this to be possible at all, you need the compiler to keep track of the particular keys and values of ext1, ext2, and config. But if you annotate those variables with wide types like Extension and Config, you will throw away the information you need to keep track of. So you can't annotate. Instead, you can use the satisfies operator to check that the variables are assignable to those types without widening to them:

在所有这些都成为可能之前,你需要编译器来跟踪ext1ext2config的特定键和值。但是,如果你使用ExtensionConfig这样宽泛的类型对这些变量进行注释,那么你将丢失跟踪所需信息。所以你不能注释。相反,你可以使用satisfies运算符检查这些变量是否可分配给这些类型而不会扩大它们:

const ext1 = {
  name: 'ext1',
  fields: {
    field1: 'test',
    field2: 'test'
  }
} satisfies Extension;

/* const ext1: {
    name: string;
    fields: {
        field1: string;
        field2: string;
    };
} */

const ext2 = {
  name: 'ext1',
  fields: {
    field3: 'test',
    field4: 'test'
  }
} satisfies Extension;

/* const ext2: {
    name: string;
    fields: {
        field3: string;
        field4: string;
    };
} */

Okay, those types know about the field names. For config we need to go a little further; it's important for extensions to keep track of the length and order of its elements, so it can distinguish between [ext1, ext2] and [Math.random()&lt;0.5 ? ext1 : ext2] (or at least I'm assuming we need to distinguish between those). That means we should use a const assertion on config and allow Config's extensions property to be a readonly array type because that's what const assertions give you:

好的,这些类型了解字段名称。对于config,我们需要再进一步;extensions跟踪其元素的长度和顺序非常重要,这样它可以区分[ext1, ext2][Math.random()&lt;0.5 ? ext1 : ext2](或者至少我假设我们需要区分它们)。这意味着我们应该在config上使用const断言,并允许Configextensions属性是一个readonly数组类型,因为这就是const断言提供的内容:

type Config = {
  name: string,
  extensions: readonly Extension[]
}
const config = {
  name: 'Test',
  extensions: [ext1, ext2]
} as const satisfies Config;

/* const config: {
    readonly name: "Test";
    readonly extensions: readonly [{
        name: string;
        fields: {
            field1: string;
            field2: string;
        };
    }, {
        name: string;
        fields: {
            field3: string;
            field4: string;
        };
    }];
} */

Okay, now that's enough information to proceed.

好的,现在有足够的信息可以继续进行。

Here's one approach:

以下是一种方法:

type ExtensionsToIntersection<T extends readonly Extension[]> =
  { [I in keyof T]: (x: T[I]["fields"]) => void }[number] extends
  (x: infer I) => void ? I : never;

declare function create<T extends Config>(config: T):
  ExtensionsToIntersection<T["extensions"]>;

The ExtensionsToIntersection<T> type takes a tuple of Extension-assignable elements and converts it to an intersection of the fields properties of those elements. The technique is very similar to that described in the answer to https://stackoverflow.com/q/74202096/2887218. In general, the idea is to map the things we want to intersect into a contravariant type position (in this case, the parameter of a function), and then infer a single type for the union of those (inferring from the parameter position of a union of functions results in an intersection of those parameter types, as described in the release notes for conditional types).

ExtensionsToIntersection<T>类型接受一个Extension可分配元素的tuple,并将其转换为这些元素的fields属性的intersection。这个技巧与https://stackoverflow.com/q/74202096/2887218的回答中描述的非常相似。总的来说,思路是将我们要交叉的内容映射到逆变类型位置(在这种情况下是函数的参数),然后推断这些联合的单一类型(从函数联合的参数位置推断会导致这些参数类型的交叉,如[条件类型的发布说明](https://www.typescriptlang.org/docs

英文:

In order for this to be possible at all, you need the compiler to keep track of the particular keys and values of ext1, ext2, and config. But if you annotate those variables with wide types like Extension and Config, you will throw away the information you need to keep track of. So you can't annotate. Instead, you can use the satisfies operator to check that the variables are assignable to those types without widening to them:

const ext1 = {
  name: &#39;ext1&#39;,
  fields: {
    field1: &#39;test&#39;,
    field2: &#39;test&#39;
  }
} satisfies Extension;

/* const ext1: {
    name: string;
    fields: {
        field1: string;
        field2: string;
    };
} */

const ext2 = {
  name: &#39;ext1&#39;,
  fields: {
    field3: &#39;test&#39;,
    field4: &#39;test&#39;
  }
} satisfies Extension;

/* const ext2: {
    name: string;
    fields: {
        field3: string;
        field4: string;
    };
} */

Okay, those types know about the field names. For config we need to go a little further; it's important for extensions to keep track of the length and order of its elements, so it can distinguish between [ext1, ext2] and [Math.random()&lt;0.5 ? ext1 : ext2] (or at least I'm assuming we need to distinguish between those). That means we should use a const assertion on config and allow Config's extensions property to be a readonly array type because that's what const assertions give you:

type Config = {
  name: string,
  extensions: readonly Extension[]
}
const config = {
  name: &#39;Test&#39;,
  extensions: [ext1, ext2]
} as const satisfies Config;

/* const config: {
    readonly name: &quot;Test&quot;;
    readonly extensions: readonly [{
        name: string;
        fields: {
            field1: string;
            field2: string;
        };
    }, {
        name: string;
        fields: {
            field3: string;
            field4: string;
        };
    }];
} */

Okay, now that's enough information to proceed.


Here's one approach:

type ExtensionsToIntersection&lt;T extends readonly Extension[]&gt; =
  { [I in keyof T]: (x: T[I][&quot;fields&quot;]) =&gt; void }[number] extends
  (x: infer I) =&gt; void ? I : never;

declare function create&lt;T extends Config&gt;(config: T):
  ExtensionsToIntersection&lt;T[&quot;extensions&quot;]&gt;;

The ExtensionsToIntersection&lt;T&gt; type takes a tuple of Extension-assignable elements and converts it to an intersection of the fields properties of those elements. The technique is very similar to that described in the answer to https://stackoverflow.com/q/74202096/2887218. In general, the idea is to map the things we want to intersect into a contravariant type position (in this case, the parameter of a function), and then infer a single type for the union of those (inferring from the parameter position of a union of functions results in an intersection of those parameter types, as described in the release notes for conditional types).

Let's test it:

const ret = create(config);
/* const ret: {
    readonly field1: &quot;test&quot;;
    readonly field2: &quot;test&quot;;
} &amp; {
    readonly field3: &quot;test&quot;;
    readonly field4: &quot;test&quot;;
} */

Looks good!

Playground link to code

huangapple
  • 本文由 发表于 2023年3月12日 10:02:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/75710687.html
匿名

发表评论

匿名网友

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

确定