英文:
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
}
有没有办法获取此返回值的正确类型?
我不一定要求使用此特定的输入格式,但有一种方法来区分相同类型的数据将会很有用(例如,能够为 int
和 float
定义不同的函数,它们最终都变成了 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: 'string',
isAdmin: 'boolean',
postCount: 'number'
});
// The function returns this:
{
bodyText: 'Lorem ipsum dolor sit amet',
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 = '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) => {
// Generate data based on field types
return data as DataReturn<T>;
}
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<T>
, 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<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. It's a recursive constraint (aka F-bounded constraint) T extends Record<keyof T, keyof DataReturn>
, using the Record<K, V>
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<string, keyof DataReturn>
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: 'string',
isAdmin: 'boolean',
postCount: 'number'
});
/* 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 "XXX"
encodes a type XXX
, then "Array<XXX>"
will be parsed so that it encodes the type XXX[]
. Like this, maybe:
type NameToType<K> =
K extends keyof DataReturn ? DataReturn[K] :
K extends `Array<${infer K0}>` ? Array<NameToType<K0>> :
never;
type CheckData<T> = { [K in keyof T]:
NameToType<T[K]> extends never ? keyof DataReturn : T[K]
}
declare function registerData<const T extends Record<keyof T, string>>(
data: T extends CheckData<T> ? T : CheckData<T>
): { [K in keyof T]: NameToType<T[K]> }
const x = registerData({
bodyText: 'string',
isAdmin: 'boolean',
postCount: 'number',
otherStuff: 'Array<string>'
});
/* 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.
答案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: () => [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 will have correct types and correct type checking on the input parameters as well.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论