[英]TypeScript: Record-style type based on generic type describing arbitrary fields and their required types
I'm trying to express my types so that when I call my code like so:我试图表达我的类型,以便当我这样调用我的代码时:
type Topics = {
documentList: Document[];
};
const TopicsContainer = useTopicsContainer<Topics>(['documentList']);
the array passed in as argument to useTopicsContainer
is constrained to the fields defined in Topics
.作为参数传递给
useTopicsContainer
的数组被限制在Topics
中定义的字段。 An example implementation of useTopicsContainer
could be as follows: useTopicsContainer
的示例实现如下:
const useTopicsContainer = <TopicsTypes>(
topics: (keyof TopicsTypes)[]
): TopicsState<TopicsTypes> => {
const initialState: TopicsState<TopicsTypes> = {};
const [topicsState, setTopicsState] = useState<TopicsState<TopicsTypes>>(
initialState
);
// some code mutating the state by calling setTopicsState, like e.g.
setInterval(
() =>
setTopicsState(
Object.fromEntries(
topics.map((topic) => [topic, createRandomDocument()])
)
),
1000
);
return topicsState;
};
I'd like to derive the TopicsState
type from the generic TopicsTypes
.我想从泛型
TopicsTypes
派生TopicsState
类型。 So far I've tried:到目前为止,我已经尝试过:
type TopicsState<TopicTypes> = Record<
keyof TopicTypes,
TopicTypes[keyof TopicTypes]
>;
but that yields a TS2322: Type '{}' is not assignable to type 'TopicsState '.
但这会产生
TS2322: Type '{}' is not assignable to type 'TopicsState '.
for const initialState: TopicsState<TopicsTypes> = {};
对于
const initialState: TopicsState<TopicsTypes> = {};
Another attempt:另一种尝试:
interface TopicsState<TopicTypes> {
[topic: keyof TopicTypes]: TopicTypes[keyof TopicTypes];
}
…but that results in TS1023: An index signature parameter type must be either 'string' or 'number'.
…但这会导致
TS1023: An index signature parameter type must be either 'string' or 'number'.
for the topic
key.为
topic
键。 I'm running out of ideas… if you'd like to give this one a go, below is a sandbox with all of the above code cobbled together.我的想法已经不多了……如果你想给这个 go,下面是一个沙箱,上面的所有代码都拼凑在一起。 Thanks!
谢谢!
As suggested by Alex Wayne , adding Partial
to TopicsState
allows an empty object literal to be a TopicsState
.正如Alex Wayne所建议的,将
Partial
添加到TopicsState
允许一个空的 object 文字成为TopicsState
。 I've updated the CodeSandbox example with that.我已经用它更新了 CodeSandbox 示例。 The only remaining issue is that now I cannot assign a value to a field in
topicsState
, getting Type '{ [k: string]: Document; }' is not assignable to type 'Partial<Record<keyof TopicsTypes, TopicsTypes[keyof TopicsTypes]>>'.ts(2345)
唯一剩下的问题是,现在我无法为
topicsState
中的字段赋值,得到Type '{ [k: string]: Document; }' is not assignable to type 'Partial<Record<keyof TopicsTypes, TopicsTypes[keyof TopicsTypes]>>'.ts(2345)
Type '{ [k: string]: Document; }' is not assignable to type 'Partial<Record<keyof TopicsTypes, TopicsTypes[keyof TopicsTypes]>>'.ts(2345)
. Type '{ [k: string]: Document; }' is not assignable to type 'Partial<Record<keyof TopicsTypes, TopicsTypes[keyof TopicsTypes]>>'.ts(2345)
。 On the bright side, the type checks on the consumer side in App.tsx
work exactly as expected:从好的方面来说,
App.tsx
中消费者端的类型检查完全符合预期:
const TopicsContainer = useTopicsContainer<Topics>(["documentList"]);
const documentList: Document[] = TopicsContainer.documentList || [];
If I change documentList: Document[]
to documentList: string
, I get the desired Type 'Document[]' is not assignable to type 'string'.ts(2322)
.如果我将
documentList: Document[]
更改为documentList: string
,我会得到所需的Type 'Document[]' is not assignable to type 'string'.ts(2322)
。 Is there a way to relax the constraint in the implementation of useTopicsContainer
?有没有办法放松
useTopicsContainer
实施中的约束?
The type I was after goes as follows:我追求的类型如下:
type ValueOf<T> = T[keyof T];
type TopicsState<TopicTypes> = Partial<
Record<keyof TopicTypes, ValueOf<TopicTypes>>
>;
with it, I can now use it like so:有了它,我现在可以像这样使用它:
type Topics = {
documentList: Document[];
};
const TopicsContainer = useTopicsContainer<Topics>(["documentList"]);
const documentList: Document[] = TopicsContainer.documentList || [];
This way, TopicsContainer
knows that under the key of "documentList"
the type is Document[]
.这样,
TopicsContainer
知道"documentList"
键下的类型是Document[]
。
For a full example checkout the sandbox:有关完整示例,请查看沙箱:
For posterity:为后代:
// useTopicsContainer.ts
import { useEffect, useState } from "react";
import io from "socket.io-client";
type ValueOf<T> = T[keyof T];
type TopicsState<TopicTypes> = Partial<
Record<keyof TopicTypes, ValueOf<TopicTypes>>
>;
type KeysOfTopicsState<TopicTypes> = Extract<keyof TopicTypes, string>[];
const client = io("/topics");
const useTopicsContainer = <TopicsTypes>(
topics: KeysOfTopicsState<TopicsTypes>
): TopicsState<TopicsTypes> => {
const initialState: TopicsState<TopicsTypes> = {};
const [topicsState, setTopicsState] = useState<TopicsState<TopicsTypes>>(
initialState
);
// some code mutating the state by calling setTopicsState, like e.g.
useEffect(() => {
const topicListeners = Object.fromEntries(
topics.map((topic) => [
topic,
(topicData: ValueOf<TopicsTypes>) => {
setTopicsState((oldState) => ({
...oldState,
[topic]: topicData
}));
}
])
);
topics.forEach((topic) => {
client.on(topic as string, topicListeners[topic]);
});
return () => {
topics.forEach((topic) => {
client.off(topic, topicListeners[topic]);
});
};
});
return topicsState;
};
export default useTopicsContainer;
// App.tsx
import React from "react";
import useTopicsContainer from "./useTopicsContainer";
type Topics = {
documentList: Document[];
};
export default function App() {
const TopicsContainer = useTopicsContainer<Topics>(["documentList"]);
const documentList: Document[] = TopicsContainer.documentList || [];
console.log({ documentList });
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
Thanks to Alex Wayne and jcalz for the suggestions, rubber ducking and keeping my examples straight!感谢Alex Wayne和jcalz的建议,橡皮擦并保持我的例子直截了当!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.