英文:
Typescript optional generic to infer the returned object type and preserve autocompletion
问题
我一直在使用TypeScript,并一直在努力解决这个问题。我正在尝试为我们的应用程序构建一个事件系统,希望在创建对象时强制执行类型安全性,这些对象用于分组与特定上下文相关的事件。
摘要
在我详细解释我想要什么之前,让我先告诉您我试图实现的最终结果:
type DashboardGroups = "home" | "settings" | "profile";
// 添加到`DashboardEventsMap`时,事件应该只包含这些组之一
type DashboardEvent = IMetricsEvent<DashboardGroups>;
// 强制事件与以下形状匹配:`{ [event: string]: DashboardEvent }`
const DashboardEventsMap = createEventMapping<DashboardEvent>({
validEvent: {
name: "valid event",
group: "home" // ✅ 这应该工作
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ❌ 这应该引发类型错误
}
})
// 但最重要的是
// 当尝试使用映射对象时,我希望保留形状和智能感知
DashboardEventsMap.validEvent // ✅ 这应该同时工作并在自动完成中显示
DashboardEventsMap.eventWhichDoesntExist // ❌ 这应该引发类型错误
详细信息
事件具有以下结构,并且必须能够接受自定义组,以强制事件属于应用程序不同部分的特定组。
export interface IMetricsEvent<
TGroup extends string = string,
> {
name: string;
group?: TGroup;
}
目前,我在createEventMapping
函数中遇到了一些问题:
type MetricsEventMapType<TEvent extends IMetricsEvent> = {
[event: string]: TEvent;
};
export const createMetricsEventMapping = <
TMetricsEvent extends IMMetricsEvent,
T extends MetricsEventMapType<TMetricsEvent> = MetricsEventMapType<TMetricsEvent>,
>(
arg: T,
) => arg;
1. 无类型
const map = createMetricsEventMapping({ event: { name: "event", group: "any group" } });
map.event // ✅ 自动完成工作,但没有类型检查组
2. 传入事件类型
但如果我传入事件类型,将会得到组的类型检查,但没有自动完成:
type DashboardEvent = IMetricsEvent<"home">;
const map = createMetricsEventMapping<DashboardEvent>({ event: { name: "event", group: "any group" } });
map.event // ❌ 类型检查在上面的组上有效 ☝️ 但不再有自动完成
3. 删除问题的 = MetricsEventMapType<TMetricsEvent>
我知道发生这种情况的原因是,当没有传递类型时,TypeScript 正确推断一切。但是,如果传递了第一个类型参数 TMetricsEvent
,TypeScript 现在期望也传递T,而由于它是可选的,它简单地默认为MetricsEventMapType<TMetricsEvent>
,而不是被推断。
但是,如果我删除默认值 = MetricsEventMapType<TMetricsEvent>
,当传递 TMetricsEventType
时,TypeScript 开始抱怨:
export const createMetricsEventMapping = <
TMetricsEvent extends IMetricsEvent,
T extends MetricsEventMapType<TMetricsEvent>, // 在这里删除默认值
>(
arg: T,
) => arg;
type DashboardEvent = IMetricsEvent<"home">;
// ❌ 现在会引发类型错误,说预期 2 个类型参数,但只传递了 1 个
const map = createMetricsEventMapping<DashboardEvent>({ event: { name: "event", group: "any group" });
如果这不可行,或者有更好的方法来处理它,而不是使用函数推断、嵌套函数或任何其他方式,请告诉我。感谢您的帮助!
英文:
I've been using TypeScript for a while and have always struggled with this. I'm trying to build an Event system for our app and I want to enforce typesafety when creating objects, which are used for grouping events that relate to a specific context together.
Summary
Before I explain in detail what I want, let me give you the final result that I'm trying to achieve:
type DashboardGroups = "home" | "settings" | "profile";
// events should only have one of these groups when added to the `DashboardEventsMap`
type DashboardEvent = IMetricsEvent<DashboardGroups>;
// enforce that events match the following shape here: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = createEventMapping<DashboardEvent>({
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ❌ This should give a type error
}
})
// BUT MOST IMPORTANTLY
// I want to preserve the shape and intellisense when trying to use the map object
DashboardEventsMap.validEvent // ✅ This should both work and show in autocomplete
DashboardEventsMap.eventWhichDoesntExist // ❌ This should give a type error
Details
Events have the following structure and must be able to take a custom group to enforce that events belong to certain groups in different parts of the app.
export interface IMetricsEvent<
TGroup extends string = string,
> {
name: string;
group?: TGroup;
}
Right now I'm running into a couple of issues with my createEventMapping
function:
type MetricsEventMapType<TEvent extends IMetricsEvent> = {
[event: string]: TEvent;
};
export const createMetricsEventMapping = <
TMetricsEvent extends IMetricsEvent,
T extends MetricsEventMapType<TMetricsEvent> = MetricsEventMapType<TMetricsEvent>,
>(
arg: T,
) => arg;
1. No type
const map = createMetricsEventMapping({event: {name:"event", group:"any group"});
map.event // ✅ autocompletion works but there is no typechecking on the groups
2. Passing in event type
but if I pass in an event type, I get the typechecking on the groups but there is no autocompletion:
type DashboardEvent = IMetricsEvent<"home">;
const map = createMetricsEventMapping<DashboardEvent>({event: {name:"event", group:"any group"});
map.event // ❌ typechecking works on the groups above ☝️ but there's no autocompletion anymore
3. Removing the culprit = MetricsEventMapType<TMetricsEvent>
I know the reason why this is happening is that when no types are passed, typescript correctly infers everything. However when the first type argument TMetricsEvent
is passed, typescript now expects T to also be passed and since it's optional, it simply defaults to MetricsEventMapType<TMetricsEvent>
instead of being inferred.
However, if I remove the default = MetricsEventMapType<TMetricsEvent>
, typescript starts yelling at me when I pass in the TMetricsEventType
(fine when no type is passed)
export const createMetricsEventMapping = <
TMetricsEvent extends IMetricsEvent,
T extends MetricsEventMapType<TMetricsEvent>, // remove default here
>(
arg: T,
) => arg;
type DashboardEvent = IMetricsEvent<"home">;
// ❌ This now gives a type error saying that expected 2 type arguments but got 1
const map = createMetricsEventMapping<DashboardEvent>({event: {name:"event", group:"any group"});
If this is not possible or are better ways to go about it with better types instead of function inference, nested functions or anything of the sort, please do let me know. Any help is appreciated!
答案1
得分: 1
如果您正在使用新版本的TypeScript,satisfies
运算符可能是一个更好的选择:
// enforce that events match the following shape here: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = {
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ✅ error now
}
} satisfies MetricsEventMapType<DashboardEvent>
// 但最重要的是
// 当尝试使用映射对象时,我希望保留形状和智能感知
DashboardEventsMap.validEvent // ✅ This should both work and show in autocomplete
DashboardEventsMap.eventWhichDoesntExist // ✅ error now
您有几种解决方法:
最简单的解决方案是使用函数柯里化:
export const createEventMapping = <
TMetricsEvent extends IMetricsEvent
>() => <
T extends Record<keyof T, TMetricsEvent>,
>(
arg: T,
) => arg;
const DashboardEventsMap = createEventMapping<DashboardEvent>()({
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ✅ error now
}
})
英文:
If you are using a new version of TypeScript, the satisfies
operator might be a better choice:
// enforce that events match the following shape here: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = {
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ✅ error now
}
} satisfies MetricsEventMapType<DashboardEvent>
// BUT MOST IMPORTANTLY
// I want to preserve the shape and intellisense when trying to use the map object
DashboardEventsMap.validEvent // ✅ This should both work and show in autocomplete
DashboardEventsMap.eventWhichDoesntExist // ✅ error now
You can use a function (if you need better control over inference) but like you already noticed TypeScript function inference is an all or nothing affair. You either let TypeScript infer all type arguments or specify them all (with the unspecified ones taken from defaults)
You have several work arounds for this:
The simplest solution is to use function currying to:
export const createEventMapping = <
TMetricsEvent extends IMetricsEvent>
() => <
T extends Record<keyof T, TMetricsEvent>,
>(
arg: T,
) => arg;
const DashboardEventsMap = createEventMapping<DashboardEvent>()({
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ✅ error now
}
})
You could also create an inference site for TMetricsEvent
as a regular argument using some sort of dummy helper function:
const type = <T,>() => null! as T
export const createEventMapping = <
TMetricsEvent extends IMetricsEvent,
T extends Record<keyof T, TMetricsEvent>,
>(
_type: TMetricsEvent,
arg: T,
) => arg;
// enforce that events match the following shape here: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = createEventMapping(type<DashboardEvent>(), {
validEvent: {
name: "valid event",
group: "home" // ✅ this should work
},
invalidEvent: {
name: "invalid event",
group: "invalid group", // ✅ error now
}
})
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论