如何重新映射函数参数的对象类型以生成所需的类型形状

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

How to remap the object types of a function argument to produce a desired type shape

问题

I am trying to resolve an issue with forcing a certain "pattern" in the framework, for the lack of a better word, that I am making. The framework in question is a form of Redux store composer (give or take).

The TS playground links:

  1. The full complex, all types included (this is the one I'd need, but it's a doozy)
  2. Simplified (minimum reproducible example - showcasing the same issue)

The gist is as follows:

I have a function which is used to create a "bundle" or a plugin for the framework named "createBundle." This function is of the type BundleInitializer, which is a complex type but suffice it to say that the return type of this function is a Bundle type.

The goal is to allow developers to call a "createBundle" function, which would then have IntelliSense support and allow for an easier adherence to the pattern that this function requires for the argument it receives.

The BundleInitializer signature semantically is as follows (simplified for clarity):

type BundleInitializer = <
  Name extends string,
  Selectors extends Record<string, Selector>,
>(
  content: {
    name: Name
    selectors: Selectors
  }
) => Bundle<Name, Selectors>

Where the type of a selector is (again for the sake of simplicity) Selector = () => unknown. This would then allow us to pass an argument that is shaped like such:

const arg = {
  name: "Example",
  selectors: {
    foo: () => {},
    bar: () => {}
  }
}

But my goal is to enforce the naming of selectors to follow a pattern of select${Capitalize<string>}. So I tried to use the following types to achieve that (the types that follow are all in the "simplified" version of the TS playground link):

type Selector = <S>(state: S) => unknown

interface Bundle<
  Name extends string,
  Selectors extends BundleSelectors<Record<string, any>>,
> {
  name: Name
  selectors: Selectors
}

type BundleInitializer = <
  Name extends string,
  Selectors extends BundleSelectors<Record<string, any>>,
>(
  content: {
    name: Name
    selectors: Selectors
  }
) => Bundle<Name, Selectors>

declare const Init: BundleInitializer

const bun = Init({
  name: "Foo", selectors: {
    wrongname: 'wrongtype',                 // CASE 1 (name incorrect, type incorrect)
    selectCorrectName: 'wrongtype',         // CASE 2 (name correct, type incorrect)
    wrongName: () => { },                   // CASE 3 (name incorrect, type correct)
    selectCorrectAll: () => { },            // CASE 4 (name correct, type correct)
  }
})

How are we supposed to define a BundleSelectors type to make sure all 4 cases are covered, I've tried these two versions, each has its own problems but neither works fully

Version 1

type BundleSelectors<T> = { [I in keyof T]: Selector }

Version 2

(type argument will be missing here, just add it if you want to check the types, it's not used anymore but other types need to be updated to reflect)

type BundleSelectors = Record<`select${string}`, Selector>;

Both of these do not produce the desired effect.

英文:

I am trying to resolve an issue with forcing a certain "pattern" in the framework, for the lack of a better word, that I am making. The framework in question is a form of Redux store composer (give or take).

The TS playground links:

  1. The full complex, all types included (this is the one I'd need, but its a doozy)
  2. Siplified (minimum reproducable example - showcasing the same issue)

The gist is as follows:

I have a function which is used to create a "bundle" or a plugin for the framework named "createBundle" this function is of the type BundleInitializer which is a complex type but suffice it to say that the return type of this functions is a Bundle type.

The goal is to allow developers to call a "createBundle" function, which would then have intelliSense support and allow for an easier adherence to the pattern that this function requires for the argument it recieves.

The BundleInitializer signature semantically is as follows (simplified for clarity):

type BundleInitializer = &lt;
  Name extends string,
  Selectors extends Record&lt;string, Selector&gt;, //
&gt;(
  content: {
    name: Name
    selectors: Selectors
  }
) =&gt; Bundle&lt;Name, Selectors&gt;

Where the type of a selector is (again for the sake of simplicity) Selector = () =&gt; unknown. This would then allow us to pass an argument that is shaped like such:

const arg = {
   name: &quot;Example&quot;,
   selectors: {
      foo: () =&gt; {},
      bar: () =&gt; {}
   }
}

But my goal is to enforce the naming of selectors to follow a pattern of select${Capitalize&lt;string&gt;}. So I tried to use the following types to achieve that (the types that follow are all in the "simplified" version of the TS playground link):

type Selector = &lt;S&gt;(state: S) =&gt; unknown


interface Bundle&lt;
  Name extends string,
  Selectors extends BundleSelectors&lt;Record&lt;string, any&gt;&gt;,
&gt; {
  name: Name
  selectors: Selectors
}

type BundleInitializer = &lt;
  Name extends string,
  Selectors extends BundleSelectors&lt;Record&lt;string, any&gt;&gt;,
&gt;(
  content: {
    name: Name
    selectors: Selectors
  }
) =&gt; Bundle&lt;Name, Selectors&gt;


declare const Init: BundleInitializer

const bun = Init({
  name: &quot;Foo&quot;, selectors: {
    wrongname: &#39;wrongtype&#39;,                 // CASE 1 (name incorrect, type incorrect)
    selectCorrectName: &#39;wrongtype&#39;,         // CASE 2 (name correct, type incorrect)
    wrongName: () =&gt; { },                   // CASE 3 (name incorrect, type correct)
    selectCorrectAll: () =&gt; { },            // CASE 4 (name correct, type correct)
  }
})

How are we suppose to define a BundleSelectors type to make sure all 4 cases are covered,
I've tried these two versions, each has its own problems but neither works fully

version 1

type BundleSelectors&lt;T&gt; = { [I in keyof T]: Selector }

version 2

type argument will be missing here, just add it if you want to check the types, its not used anymore but other types need to be updated to reflect

type BundleSelectors = Record&lt;`select${string}`, Selector&gt;

Both of these do not produced the desired effect.

答案1

得分: 1

以下是您提供的内容的翻译部分:

一种方法是定义BundleInitializer,使与selectors对应的通用类型参数递归地受限,以便拒绝任何不匹配 `select${string}` 的属性键。这将涉及到映射类型,以使具有良好键的属性具有类型Selector,而具有不良键的属性具有类似于不可能的never类型的类型,这与任何可能传递的值都不兼容。让我们将该类型称为BadKey<K>,它接受类型为K的键并生成不兼容的类型。

然后,BundleInitializer将被定义为

type BundleInitializer = <
  N extends string,
  S extends { [K in keyof S]: K extends `select${string}` ? Selector : BadKey<K> }
>(content: { name: N; selectors: S }) => Bundle<N, S>;

您将获得所需的行为:

const bun = Init({
  name: "Foo", selectors: {
    wrongname: 'wrongtype',                 // 错误
    selectCorrectName: 'wrongtype',         // 错误
    wrongName: () => { },                   // 错误
    selectCorrectAll: () => { },            // 正确
  }
})

所以现在只剩下定义BadKey。我们可以将其轻松定义为never

type BadKey<K extends PropertyKey> = never;

但然后错误消息会说属性不是never,这会令人困惑。理想情况下,TypeScript会提供一个“无效”类型,其类型类似于never,但提供了用户友好的错误消息,但目前TypeScript没有这样的功能。在microsoft/TypeScript#23689上有一个开放的功能请求。如果它存在,也许我们可以编写:

type BadKey<K extends PropertyKey> = Invalid<
  `You provided a key of "${Exclude<K, symbol>}" \
  but the keys all need to be of the form \`select${string}\``
>;

但它目前不存在。目前,我们只能做类似于以下的事情:

type BadKey<K extends PropertyKey> = `You provided a key of "${Exclude<K, symbol>}" \
  but the keys all need to be of the form \`select${string}\``;

并依赖于传递的属性值几乎不可能是该错误字符串。因此,我们会得到像以下的错误:

Type '() => void' is not assignable to type 
  '`You provided a key of "wrongName" but the keys all 
  need to be of the form \`select${string}\`' (2322)

这希望足够让人们理解应该怎么做。

英文:

One approach is to define BundleInitializer so that the generic type parameter corresponding to selectors is recursively constrained so that any property key that doesn't match `select${string}` is rejected in some way. That would involve mapping the type so that properties with good keys have type Selector, while those with bad keys have a type like the impossible never type which is not compatible with any value likely to be passed in. Let's call that type BadKey&lt;K&gt; which takes a key of type K and produces an incompatible type.

Then BundleInitializer would be defined as

type BundleInitializer = &lt;
  N extends string,
  S extends { [K in keyof S]: K extends `select${string}` ? Selector : BadKey&lt;K&gt; }
&gt;(content: { name: N; selectors: S }) =&gt; Bundle&lt;N, S&gt;

and you'd get the desired behavior:

const bun = Init({
  name: &quot;Foo&quot;, selectors: {
    wrongname: &#39;wrongtype&#39;,                 // error
    selectCorrectName: &#39;wrongtype&#39;,         // error
    wrongName: () =&gt; { },                   // error
    selectCorrectAll: () =&gt; { },            // okay
  }
})

So all that remains is defining BadKey. We can trivially define it as never,

type BadKey&lt;K extends PropertyKey&gt; = never;

but then the error message just says that the property isn't never as expected, which is confusing. Ideally TypeScript would provide an "invalid" type whose type is never-like but which provides a user-friendly error message, but TypeScript doesn't currently have such a facility. There's an open feature request for it at microsoft/TypeScript#23689. If it existed maybe we could write

type BadKey&lt;K extends PropertyKey&gt; = Invalid&lt;
  `You provided a key of &quot;${Exclude&lt;K, symbol&gt;}&quot; \
  but the keys all need to be of the form \`select${string}\``
&gt;;

but it doesn't. For now, all we can do is something like

type BadKey&lt;K extends PropertyKey&gt; = `You provided a key of &quot;${Exclude&lt;K, symbol&gt;}&quot; \
  but the keys all need to be of the form \`select${string}\``;

and rely on the fact that whatever the property value passed in is very unlikely to be that error string. So we get errors like:

Type &#39;() =&gt; void&#39; is not assignable to type 
  &#39;`You provided a key of &quot;wrongName&quot; but the keys all 
  need to be of the form \`select${string}\``&#39;.(2322)

which is hopefully enough for people to understand what to do.

Playground link to code

huangapple
  • 本文由 发表于 2023年7月18日 05:38:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76708236.html
匿名

发表评论

匿名网友

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

确定