如何定义具有重载的函数以具有恰好 1 或 2 个参数,其类型取决于使用的字符串文字

How to define function with overloads to have exactly 1 or 2 arguments which types depend on used string literal

我想实现一个 emitEvent(event, extra?) 函数,该函数将受已知字符串的字符串文字枚举约束,例如 POPUP_OPENPOPUP_CLOSED 等。此函数接受第二个参数,这又是一个明确定义的字典形状,只能与特定事件键一起使用。

这是所有已知事件的字典:

interface Events {
    POPUP_OPEN: {name:'POPUP_OPEN',extra: {count:number}},
    POPUP_CLOSED: {name:'POPUP_CLOSED'},
    AD_BLOCKER_ON: {name:'AD_BLOCKER_ON', extra: {serviceName:string}},
    AD_BLOCKER_OFF: {name:'AD_BLOCKER_OFF'}
}

具有要求的类型约束的用法:

// $ExpectType  {object: string; action: string; value: string;}
const t1 = emitEvent('POPUP_CLOSED')

// $ExpectError -> no extra argument allowed
const t11 = emitEvent('POPUP_CLOSED', {what:'bad'})

// $ExpectType  {object: string;action: string;foo: string; count: number;}  
const t2 = emitEvent('POPUP_OPEN',{count: 1231})

// $ExpectError -> extra argument is missing
const t22 = emitEvent('POPUP_OPEN')  

我的实现:

这个实现有一个大问题,对于字典值,定义了extra,没有定义时TS不会报错

// ✅ NO ERROR
const t2 = emitEvent('POPUP_OPEN',{count: 1231})
// ✅ Error
const t2 = emitEvent('POPUP_OPEN',{})
// NO ERROR -> THIS SHOULD ERROR !
const t2 = emitEvent('POPUP_OPEN')

实施:

type ParsedEvent<Extra = void> = Extra extends object ? BaseParsedEvents & Extra : BaseParsedEvents

type BaseParsedEvents = {
  object:string
  action:string
  value:string
}

type KnownEvents = keyof Events
type GetExtras<T> = T extends {name: infer N, extra: infer E} ? E : never;

function emitEvent<T extends KnownEvents>(event:T): ParsedEvent
function emitEvent<T extends KnownEvents, E extends GetExtras<Events[T]>>(event:T, extra: E): ParsedEvent<E>
function emitEvent<T extends string, E extends object>(event:T, extra?: E) {
  const parsedEmit = parse(event)
  return {...parsedEmit,...extra}
}


function parse(event:string): BaseParsedEvents {
    const [object,action,value] = event.split('_')  
    return {object,action,value}
}

总的来说,恐怕这是不可能实现的,但希望我是错的:)

您可以使用 tuples in rest parameters 而不是重载来让函数接受可变数量的参数。 GetExtras 将 return 具有单个 E 元素的元组或空元组。然后我们可以将GetExtras作为函数的spread参数展开:

interface Events {
    POPUP_OPEN: {name:'POPUP_OPEN',extra: {count:number}},
    POPUP_CLOSED: {name:'POPUP_CLOSED'},
    AD_BLOCKER_ON: {name:'AD_BLOCKER_ON', extra: {serviceName:string}},
    AD_BLOCKER_OFF: {name:'AD_BLOCKER_OFF'}
}


// $ExpectType  {object: string; action: string; value: string;}
const t1 = emitEvent('POPUP_CLOSED')

// ✅ NO ERROR
const t21 = emitEvent('POPUP_OPEN',{count: 1231})
// ✅ Error
const t213 = emitEvent('POPUP_OPEN',{})
// ✅  ERROR  as expected
const t223 = emitEvent('POPUP_OPEN')


type ParsedEvent<Extra extends [object] | [] = []> = Extra extends [infer E] ? BaseParsedEvents & E : BaseParsedEvents

type BaseParsedEvents = {
  object:string
  action:string
  value:string
}

type KnownEvents = keyof Events
type GetExtras<T> = T extends {name: infer N, extra: infer E} ? [E] : [];

function emitEvent<T extends KnownEvents, E extends GetExtras<Events[T]>>(event:T, ...extra: E): ParsedEvent<E>
function emitEvent<T extends string, E extends object>(event:T, extra?: E) {
  const parsedEmit = parse(event)
  return {...parsedEmit,...extra}
}


function parse(event:string): BaseParsedEvents {
    const [object,action,value] = event.split('_')  
    return {object,action,value}
}

play

如何将您的第一个函数重载声明限制为不包含 extra 属性 的事件?

// define events, that do not have extra property
// "POPUP_CLOSED" | "AD_BLOCKER_OFF"
type KnownEventsWithoutExtra = {
  [K in keyof Events]: Events[K] extends { name: string; extra: object }
    ? never
    : K
}[keyof Events];

// single argument overload is restricted to above events
function emitEvent<T extends KnownEventsWithoutExtra>(event: T): ParsedEvent;

// will error now
const t4 = emitEvent("POPUP_OPEN");

Playground