英文:
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:
- The full complex, all types included (this is the one I'd need, but it's a doozy)
- 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:
- The full complex, all types included (this is the one I'd need, but its a doozy)
- 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 = <
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 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<T> = { [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<`select${string}`, Selector>
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<K>
which takes a key of type K
and produces an incompatible type.
Then BundleInitializer
would be defined as
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>
and you'd get the desired behavior:
const bun = Init({
name: "Foo", selectors: {
wrongname: 'wrongtype', // error
selectCorrectName: 'wrongtype', // error
wrongName: () => { }, // error
selectCorrectAll: () => { }, // okay
}
})
So all that remains is defining BadKey
. We can trivially define it as never
,
type BadKey<K extends PropertyKey> = 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<K extends PropertyKey> = Invalid<
`You provided a key of "${Exclude<K, symbol>}" \
but the keys all need to be of the form \`select${string}\``
>;
but it doesn't. For now, all we can do is something like
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}\``;
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 '() => 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)
which is hopefully enough for people to understand what to do.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论