TypeScript 泛型回调参数类型推断

时间:2021-06-10 12:20:18

标签: typescript typescript-generics

我正在努力解决如何强输入我的消息广播类

基本上我想发送和收听一组有限的消息,但似乎不可能输入解决方案的设计方式:

签到Typescript Playground

代码:


interface Messages {
    'navigation.start': {
        path: string
    },
    'navigation.error': {
        path: string,
        reason: 404 | 403
    },
    'auth.loggedin' : {
        user: string,
    }
}

type AvailableMessages = keyof Messages;

type Listeners = {
    [K in AvailableMessages]: Array<(payload: Messages[K]) => void>
}

class Messaging {
    private listeners: Listeners = {
        'navigation.start': [],
        'navigation.error': [],
        'auth.loggedin': [],
    };

    public listen<K extends AvailableMessages, M = Messages[K]>(message: K, callback: (payload: M) => void) {
        this.listeners[message].push(callback);
    }

    public send<K extends AvailableMessages, M = Messages[K]>(message: K, payload: M) {
        const messageListeners = this.listeners[message];
        if(messageListeners == null) {
            return;
        }

        messageListeners.forEach(l => l(payload));

    }
}

new Messaging().listen('navigation.start', () => { console.log('?') });
new Messaging().listen('navigation.start', (a) => { console.log(a.path) });
new Messaging().listen('navigation.error', (a) => { console.log(a.reason) });
new Messaging().listen('auth.loggedin', (a) => { console.log(a.user) });


1 个答案:

答案 0 :(得分:0)

方法签名有两个问题

public listen<K extends AvailableMessages, M = Messages[K]>(
     message: K,
     callback: (payload: M) => void
)

首先,M 的类型没有限制,所以我可以做

new Messaging().listen<'navigation.start', number>('navigation.start', 3)

这不是类型安全的。

其次,即使删除了类型参数 M(即 callback 的类型为 (payload: Messages[K]) => void

new Messaging()
    .listen<'navigation.start' | 'navigation.error'>(
        'navigation.start',
        ({ reason }) => console.log("navigation.error listener called")
    )

这会违反类型安全。


理想情况下,可以通过某种方式指定泛型参数恰好是键之一(即没有联合类型)。据我所知,在 TypeScript 中并没有真正做到这一点的好方法。

可以通过滥用分配条件类型来创建将联合类型转换为 never 的类型:

type SingleHelper<T, V> = T extends any 
    ? [V] extends [T] 
        ? V 
        : never 
    : never;
type Single<T> = SingleHelper<T, T>

使用这种类型,Single<"hello"> 的计算结果为 "hello",但 Single<"hello" | "goodbye"> 的计算结果为 never

然而,即使我们在 listen 的签名中使用了这种类型,即如果我们写了

public listen<K extends AvailableMessages>(
     message: Single<K>,
     callback: (payload: Messages[Single<K>]) => void
)

TypeScript 无法推断该类型仅包含确切的键。虽然您仍然会在正文中遇到类型错误,但它会迫使调用者正确使用该函数。