typescript 用于推断返回的对象类型并保留自动完成功能的可选泛型

htrmnn0y  于 2023-02-25  发布在  TypeScript
关注(0)|答案(1)|浏览(109)

我使用TypeScript已经有一段时间了,一直在努力解决这个问题。我正在尝试为我们的应用构建一个事件系统,我希望在创建对象时强制类型安全,这些对象用于将与特定上下文相关的事件分组在一起。

总结

在我详细解释我想要什么之前,让我给你我试图达到的最终结果:

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

详细信息

事件具有以下结构,并且必须能够采用自定义组,以强制事件属于应用程序不同部分中的特定组。

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 IMetricsEvent,
  T extends MetricsEventMapType<TMetricsEvent> = MetricsEventMapType<TMetricsEvent>,
>(
  arg: T,
) => arg;
1.没有类型
const map = createMetricsEventMapping({event: {name:"event", group:"any group"});

map.event // ✅ autocompletion works but there is no typechecking on the groups
2.传入事件类型

但是如果我传入一个事件类型,我会得到对组的类型检查,但是没有自动完成:

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.删除问题= MetricsEventMapType<TMetricsEvent>

我知道发生这种情况的原因是当没有类型被传递时,typescript正确地推断出所有的东西,但是当第一个类型参数TMetricsEvent被传递时,typescript现在期望T也被传递,因为它是可选的,所以它只是默认为MetricsEventMapType<TMetricsEvent>而不是被推断出来。
但是,如果我删除默认的= MetricsEventMapType<TMetricsEvent>,那么当我传入TMetricsEventType时,typescript就会开始对我大喊大叫(当没有传入类型时,就很好)

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"});

如果这是不可能的,或者是用更好的类型来代替函数推理、嵌套函数或任何类似的东西的更好的方法,请让我知道。任何帮助都是感激之情!

vc6uscn9

vc6uscn91#

如果您使用的是新版本的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>

// 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

Playground链接
你可以使用一个函数(如果你需要更好地控制推理),但是就像你已经注意到的,TypeScript函数推理是一件全有或全无的事情。你要么让TypeScript推理所有的类型参数,要么指定它们(未指定的参数取自默认值)
您有几种解决方法:
最简单的解决方案是使用函数currying:

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
  }
})

Playground链接
您还可以使用某种虚拟助手函数为TMetricsEvent创建一个推理站点,作为常规参数:

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
  }
})

Playground链接

相关问题