Typescript可选泛型以推断返回的对象类型并保留自动完成

huangapple go评论53阅读模式
英文:

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 = &quot;home&quot; | &quot;settings&quot; | &quot;profile&quot;;
// events should only have one of these groups when added to the `DashboardEventsMap` 
type DashboardEvent = IMetricsEvent&lt;DashboardGroups&gt;;

// enforce that events match the following shape here: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = createEventMapping&lt;DashboardEvent&gt;({
  validEvent: {
    name: &quot;valid event&quot;,
    group: &quot;home&quot; // ✅ this should work
  },
  invalidEvent: {
    name: &quot;invalid event&quot;,
    group: &quot;invalid group&quot;, // ❌ 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&lt;
  TGroup extends string = string,
&gt; {
  name: string;
  group?: TGroup;
}

Right now I'm running into a couple of issues with my createEventMapping function:

type MetricsEventMapType&lt;TEvent extends IMetricsEvent&gt; = {
  [event: string]: TEvent;
};

export const createMetricsEventMapping = &lt;
  TMetricsEvent extends IMetricsEvent,
  T extends MetricsEventMapType&lt;TMetricsEvent&gt; = MetricsEventMapType&lt;TMetricsEvent&gt;,
&gt;(
  arg: T,
) =&gt; arg;

1. No type

const map = createMetricsEventMapping({event: {name:&quot;event&quot;, group:&quot;any group&quot;});

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&lt;&quot;home&quot;&gt;;
const map = createMetricsEventMapping&lt;DashboardEvent&gt;({event: {name:&quot;event&quot;, group:&quot;any group&quot;});

map.event // ❌ typechecking works on the groups above ☝️ but there&#39;s no autocompletion anymore

3. Removing the culprit = MetricsEventMapType&lt;TMetricsEvent&gt;

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&lt;TMetricsEvent&gt; instead of being inferred.

However, if I remove the default = MetricsEventMapType&lt;TMetricsEvent&gt;, typescript starts yelling at me when I pass in the TMetricsEventType (fine when no type is passed)

export const createMetricsEventMapping = &lt;
  TMetricsEvent extends IMetricsEvent,
  T extends MetricsEventMapType&lt;TMetricsEvent&gt;, // remove default here
&gt;(
  arg: T,
) =&gt; arg;

type DashboardEvent = IMetricsEvent&lt;&quot;home&quot;&gt;;
// ❌ This now gives a type error saying that expected 2 type arguments but got 1
const map = createMetricsEventMapping&lt;DashboardEvent&gt;({event: {name:&quot;event&quot;, group:&quot;any group&quot;});

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

[Playground Link](https://www.typescriptlang.org/play?#code/C4TwDgpgBAIghgZwBYCMD2cBOATA4ptAVzASgF4oAiJNAWwkqgB8qEJhgBLAOwHMFGLSmAIAzTgBsGAbgCwAKAD0iqBABuEbsFLIiE7FDTcJIKEjgbD3aGlFRgSCGyi8CxUgHdH3KHGzYIA2A0e0coAAN4ZHQsbABRDS0EAFk4MHCoBVBIWERUDBwEzWByKABJZPZMTgBjBCKtAB4o-Nj8IhIAPjl5BQUaowQS7OgKRoAVABpOgAoASnJOqG5CCQkAQl9ScYUIAA8wNEwSge4hqBrMCDhgCAbgVLAwHl5SxoUoT6hxyuBquvuqj2t242FIFSqtXqiWAkw+X3GQJBYKgACUIAMcI0ANYQEC2b6Tb6-f7Q4qdOHyT6zeGfAD6IwAXMTIQCYZSvr5MLxmVN4QsyEssLwen0lCpNKIjjVoA4bqoYaRaDcakhQtApWs0B4XlBkGloI4rszwgBvKAAbXUxUZQ2qfAAusyWjFCjCoABfcL9QYlF0FeKKx6lU3wtRwCScQM2qChqlfbhwejMyjhyMGa1aSgcr6uDopmj0RjKKCAUHJQpwdDRVgYPEdsfCPTmeGmo-dmXHOYnk1QWxGowritnaS43GAU3306OOtmoCXyxBMARMMttY2FB69TdK+InFASVD7o9xuAIM08q7o1pOmKSwAhACqiOSAHkAMqIioABRfqPGAEEADlxgAGQATQUEsyigDw4C0ewQhEJxF0sBxoH1HI4IMHhbjWStNGcLxNHsTAQF1YIoEIZw0KgZUwEMFAACsMWABR-Vie4UjSAA6VsrxKedviQSs9WrfQoHQBwYPrXxQVE7UoB4XxCGCAZaDAKRbjYi8A04x5uMzYAAHVhNVGA0CcLQ4j2SsBJUBclyOVcPDFfZDmORStEXUQ4BlcoDzZYp3njcZ2mIJFNBRO1dQoaK+EpJZO2WJMIGZOKRXhPNiAAfl5MKwB6D0xSyU991V2vU6nUgA,但正如您已经注意到的,TypeScript函数推断是一种全盘托付的事情。您要么让TypeScript推断所有类型参数,要么指定它们(未指定的类型参数将从默认值中获取)。

您有几种解决方法:

最简单的解决方案是使用函数柯里化:

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 Link](https://www.typescriptlang.org/play?#code/C4TwDgpgBAIghgZwBYCMD2cBOATA4ptAVzASgF4oAiJNAWwkqgB8qEJhgBLAOwHMFGLSmAIAzTgBsGAbgCwAKAD0iqBABuEbsFLIiE7FDTcJIKEjgbD3aGlFRgSCGyi8CxUgHdH3KHGzYIA2A0e0coAAN4ZHQsbABRDS0EAFk4MHCoBVBIWERUDBwEzWByKABJZPZMTgBjBCKtAB4o-Nj8IhIAPjl5BQUIAA8wNEwSmqMEMcwIOGAIBuBUsDAeXlLGhSgoABVK4Gq6hdUBue5sUgqq2vrE4E7NqAAKAEpyTqgN+S2t7ePT86gACUIOMcI0ANYQEC2HYAGh2ewON2KnVhD06jweWywvAAXHCHq8yO8cT0FMpVNxRCMatAHLNVLdSLRZjUkKFoNSJBI0B5VlBkGloI5pvjwgBvKAAbXUxVxk2qfAAuviWjFCrcoABfcIKcbcSa5aIFeJMpalGrTWbzW5LFZ8Zp5dWmlEvR7ih5qOASTgurT4j1fb7cOD0fGUL0+gyyrSUNFBrauDrhmj0RgUwCg5KFODoaIR9FAPCNwQ8tfGtjxI76FgGsVAQ2GqJXvb7GcU43Wk8Rw82oy

英文:

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: &quot;valid event&quot;,
    group: &quot;home&quot; // ✅ this should work
  },
  invalidEvent: {
    name: &quot;invalid event&quot;,
    group: &quot;invalid group&quot;, // ✅ error now
  }
} satisfies MetricsEventMapType&lt;DashboardEvent&gt;

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

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 = &lt;
  TMetricsEvent extends IMetricsEvent&gt;
  () =&gt; &lt;
    T extends Record&lt;keyof T, TMetricsEvent&gt;,
  &gt;(
    arg: T,
  ) =&gt; arg;

const DashboardEventsMap = createEventMapping&lt;DashboardEvent&gt;()({
  validEvent: {
    name: &quot;valid event&quot;,
    group: &quot;home&quot; // ✅ this should work
  },
  invalidEvent: {
    name: &quot;invalid event&quot;,
    group: &quot;invalid group&quot;, // ✅ error now
  }
})

Playground Link

You could also create an inference site for TMetricsEvent as a regular argument using some sort of dummy helper function:

const type = &lt;T,&gt;() =&gt; null! as T
export const createEventMapping = &lt;
    TMetricsEvent extends IMetricsEvent,
    T extends Record&lt;keyof T, TMetricsEvent&gt;,
  &gt;(
    _type: TMetricsEvent,
    arg: T,
  ) =&gt; arg;


// enforce that events match the following shape here: `{ [event:string]: DashboardEvent }`
const DashboardEventsMap = createEventMapping(type&lt;DashboardEvent&gt;(), {
  validEvent: {
    name: &quot;valid event&quot;,
    group: &quot;home&quot; // ✅ this should work
  },
  invalidEvent: {
    name: &quot;invalid event&quot;,
    group: &quot;invalid group&quot;, // ✅ error now
  }
})

Playground Link

huangapple
  • 本文由 发表于 2023年2月23日 19:46:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/75544401.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定