在 TypeScript 中,是否可以从字符串的输入类型推断出可区分联合的字符串文字类型? [英] In TypeScript is it possible to infer string literal types for Discriminated Unions from input type of string?

查看:30
本文介绍了在 TypeScript 中,是否可以从字符串的输入类型推断出可区分联合的字符串文字类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

背景

当前在我的 TypeScript 应用程序中创建事件如下所示:

<预><代码>//这个函数创建事件创建者functiondefineEvent(type:E["type"]) {return (payload:E["payload"]) =>({类型,有效载荷})}//此函数创建 foo 事件const fooCreator =defineEvent<{类型:富",有效载荷:{富:字符串}}>("foo");//此函数创建柱线事件const barCreator =defineEvent<{类型:酒吧",有效载荷:{酒吧:字符串}}>("酒吧");//示例事件const fooEvent = fooCreator({foo:'foo'});const barEvent = barCreator({bar:'bar'});//创建一个联合类型来切换type AppEvent = ReturnType|ReturnType;//使用可区分联合进行切换的示例函数 switchOnEvents(事件:AppEvent){开关(事件.类型){案例foo"://编译器对有效载荷具有 foo 属性感到高兴返回 event.payload.foo.toLowerCase();案例酒吧"://编译器对具有 bar 属性的负载感到高兴返回 event.payload.bar.toLowerCase();}}

问题

这有效并且很好,但这意味着我需要在定义事件创建者时两次指定事件类型,这在某些方面可能被认为是多余的.

const fooCreator =defineEvent<{type:"foo",//在这里定义类型有效载荷:{富:字符串}}>("foo");//也在这里创建实际的字符串值

是否可以为创建者函数创建一个返回类型,该函数从工厂函数的输入 type 参数中提取字符串文字?

这意味着如果事件创建者是这样提供的,上面的例子仍然有效:

const barCreator =defineEvent<{有效载荷:{酒吧:字符串}}>("酒吧");

尝试失败

我猜我需要合并两种类型,但以某种方式推断字符串文字.

但是这不起作用:

functiondefineEvent(type:ET) {类型 RE = E &{类型:ET};return (payload:E["payload"]) =>{返回 ({类型,有效载荷} as any ) as RE}}

编译器要我传递第二个类型参数.

const fooCreator =defineEvent<{有效载荷:{富:字符串}//预期有2个类型参数,但得到了1个.}>("foo");

有谁知道如何做到这一点,或者如果这是不可能的?

解决方案

这里的问题是 TypeScript 还不支持 部分类型参数推断;您的 defineEvent() 函数从根本上取决于两种类型:有效载荷类型(称之为 P),以及,呃,type"字符串文字类型(称之为 >T).您想指定 P 但让编译器推断 T,这是不支持的.您可以同时指定它们,或者编译器可以尝试推断它们.

<小时>

所以我知道有两种可能的解决方法.一种是使用柯里化,其中一个函数返回另一个函数.第一个函数在 P 中是泛型的,你将指定它,返回的函数在 T 中是泛型的,这将从函数的参数中推断出来.像这样:

const defEvent = 

() =><K扩展字符串>(类型:K)=>(有效载荷:P) =>({类型,有效载荷});const fooCreator = defEvent<{ foo: string }>()("foo");const barCreator = defEvent<{ bar: string }>()("bar");

这为您提供了与之前相同的 fooCreatorbarCreator 对象,并且您可以准确指定负载类型和类型字符串.这里的尴尬在于额外的链式函数调用.

<小时>

另一种解决方法是使用一个虚拟负载参数,它允许编译器推断 TP.defineEvent() 函数体不使用虚拟参数;它只被类型系统使用.这也意味着您实际上并不需要传入 P 类型的实际值;您可以使用 type assertion 传递类似nullundefined:

const defEvent = (payloadDummy: P, type: T) =>(有效载荷:P) =>({类型,有效载荷});const fooCreator = defEvent(null! as { foo: string }, "foo");const barCreator = defEvent(null! as { bar: string }, "bar");

这又是你所拥有的 fooCreatorbarCreator.当然,这里的尴尬在于使用虚拟参数.我更喜欢柯里化而不是虚拟化,但这取决于你.

<小时>

好的,希望有帮助;祝你好运!

链接到代码

Background

Currently creating events in my TypeScript app looks like this:


// This function creates event creators
function defineEvent<E extends {type:string, payload:any}>(type:E["type"]) {
  return (payload:E["payload"]) => ({
    type,
    payload
  })
}

// This function creates foo events
const fooCreator = defineEvent<{
  type:"foo", 
  payload: {
    foo:string
  }
}>("foo");

// This function creates bar events 
const barCreator = defineEvent<{
  type:"bar", 
  payload:{
    bar:string
  }
}>("bar");

// Example events
const fooEvent = fooCreator({foo:'foo'});
const barEvent = barCreator({bar:'bar'});

// Create a union type to switch over
type AppEvent = ReturnType<typeof fooCreator> | ReturnType<typeof barCreator>;

// Example of switching with a discriminated union
function switchOnEvents(event: AppEvent) {
  switch(event.type){
    case "foo": 
      // compiler is happy about payload having a foo property
      return event.payload.foo.toLowerCase();
    case "bar":
      // compiler is happy about payload having a bar property
      return event.payload.bar.toLowerCase();
  } 
}

Question

This works and is okish however this means I need to specify the event type twice when defining event creators which in some ways could be considered redundant.

const fooCreator = defineEvent<{
  type:"foo", // defining type here
  payload: {
    foo:string
  }
}>("foo"); // also here to create the actual string value

Is it possible to create a return type for the creator function that extracts the string literal from the input type argument to the factory function?

Which would mean the example above would still work if the event creators were provided like so:

const barCreator = defineEvent<{
  payload:{
    bar:string
  }
}>("bar");

Failed Attempt

I am guessing I need to merge two types but somehow infer the string literal.

However this doesn't work:

function defineEvent<E extends { payload:any}, ET extends string>(type:ET) {
  type RE = E & {type: ET};
  return (payload:E["payload"]) => {
    return ({
      type,
      payload
    } as any ) as RE
  }
}

The compiler wants me to pass a second type argument.

const fooCreator = defineEvent<{
  payload: {
    foo:string
  } //Expected 2 type arguments, but got 1.
}>("foo");

Anyone know how to do this or if it is impossible?

解决方案

The problem here is that TypeScript does not yet support partial type parameter inference; your defineEvent() function fundamentally depends on two types: the payload type (call it P), and the, uh, "type" string literal type (call it T). You want to specify P but have the compiler infer T, and that's not supported. You can either specify both of them, or the compiler can try to infer both of them.


So there are two possible workarounds I know of. One is to use currying, where a function returns another function. The first function is generic in P, which you will specify, and the returned function is generic in T, which will be inferred from the function's argument. Like this:

const defEvent = <P>() => <K extends string>(type: K) => (payload: P) => ({
  type,
  payload
});

const fooCreator = defEvent<{ foo: string }>()("foo");
const barCreator = defEvent<{ bar: string }>()("bar");

This gives you the same fooCreator and barCreator objects you had before, and you specify exactly the payload type and the type string. The awkwardness here is in the extra chained function call.


The other workaround is to use a dummy payload parameter which allows the compiler to infer both T and P. The dummy parameter is not used by the body of the defineEvent() function; it's only used by the type system. That also means you don't really need to pass an actual value of type P in; you can use a type assertion to pass something like null or undefined:

const defEvent = <P, T extends string>(payloadDummy: P, type: T) => (
  payload: P
) => ({
  type,
  payload
});

const fooCreator = defEvent(null! as { foo: string }, "foo");
const barCreator = defEvent(null! as { bar: string }, "bar");

This is again, the same fooCreator and barCreator you had. The awkwardness here, of course, is using the dummy parameters. I tend to prefer currying over dummying, but that's up to you.


Okay, hope that helps; good luck!

Link to code

这篇关于在 TypeScript 中,是否可以从字符串的输入类型推断出可区分联合的字符串文字类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆