网站搜索

typescript - 基于另一个成员来缩小成员类型


type Event<S extends string> = {
    name: S
}

const EventHandler = <S extends string, T extends Event<S>[]>(arr: [...T]) => {
    let lastEvent: T[number] = arr[0]
    return {
        set: (event: T[number]) => lastEvent = event,
        emit: (event: typeof lastEvent['name']) => { },
        lastEvent
    }
}

const emitter = EventHandler([{ name: 'A' }, { name: 'B' }])

if(emitter.lastEvent.name === 'A') {
    // `emitter.lastEvent` is now inferred to `{ name: A }`
    
    emitter.emit('B') // compiles, but I want it to only allow the string `A`
}

在这个人为的例子中,我创建了一个对象 EventHandler,它有一些配置 lastEvent。现在,我想根据配置来缩小 emit 的参数范围。

然而,即使在条件语句中 lastEvent 的类型被缩小,我仍然无法因此缩小 emit 的输入类型。

这是可能的吗?最好不要使用用户定义的类型保护...

[编辑]:

我以为我可以做出妥协,并将事件对象作为输入添加到 emit 中:

emit: (event: T[number], name: typeof event['name']) => { }

然后做:

emitter.emit({ name: 'A' }, 'B') // why does this compile ?!

但这也无法被推断!这看起来很奇怪——为什么 TypeScript 无法处理这个?

答案#1

为了解决这个问题,我们理想情况下会构建一个区分联合类型,这样通过检查 lastEvent 的类型就可以缩小 emit 函数的类型。这将要求我们检查 emitter.lastEvent.name 的嵌套值。但这是一个问题,因为 TypeScript 目前不允许基于深层嵌套属性来缩小区分联合类型(参见 #18758)。

我们现在可以做的是避免嵌套,并将name的字符串值放在对象的顶层。

我们可以使用 T[number] 的联合类型与映射类型来构建一个判别联合,其中每个联合成员 K 是判别标志。

const EventHandler = <T extends string[]>(arr: [...T]) => {
    let lastEvent: T[number] = arr[0]
    return {
        set: (event: T[number]) => lastEvent = event,
        emit: (event: typeof lastEvent) => { },
        lastEvent
    } as {
        set: (event: T[number]) => void 
    } & {
        [K in T[number]]: {
            emit: (event: K) => void
            lastEvent: K
        }
    }[T[number]]
}

这让我们能够通过检查 lastEvent 的值来正确地缩小 emit 的类型。

const emitter = EventHandler(['A', 'B'])

if(emitter.lastEvent === 'A') {    
    emitter.emit('A')
    

    emitter.emit('B') // Error: Argument of type '"B"' is not assignable 
                      // to parameter of type '"A"'
}

if(emitter.lastEvent === 'B') {
    emitter.emit('A') // Error: Argument of type '"A"' is not assignable 
                      // to parameter of type '"B"'
    
    emitter.emit('B')
}

emitter.set('A')
emitter.set('B')

操场