如何从具有类型约束的泛型函数正确推断 RxJS Subject<T> 的 T

How to correctly infer T for an RxJS Subject<T> from generic function with type constraint

我有一个模块可以让呼叫者订阅一组离散的事件。当调用者订阅给定事件时,他们提供事件名称作为第一个参数。从那个论点,我希望能够推断出回调签名。

该实现为每个受支持的事件使用 RxJS Subject,并为每次调用 myModule.subscribe(eventType) 创建对其的订阅。下面显示了实施的精简版本(您还可以在 this TS Playground workspace 上看到这个 运行)

import { Subject, Subscription } from "rxjs";

const EventNames = {
    a: "a",
    b: "b",
    c: "c",
} as const;

type Payloads = {
    [EventNames.a]: string;
    [EventNames.b]: number;
    [EventNames.c]: boolean;
};

type EventTypes = keyof typeof EventNames;

// all possible event objects
type Events<T> = T extends EventTypes ? {type: T, payload: Payloads[T]} : never;

// all possible callbacks
type Callback<T> = T extends EventTypes ? (event: Events<T>) => void : never;

// all possible subjects
type Subjects<T> = T extends EventTypes ? Subject<Events<T>> : never;

const collection = new Map<EventTypes, Subjects<EventTypes>>();

function fnWithConstraint<T extends EventTypes>(
    name: T,
    cb: Callback<T>
): Subscription | null {
    const subject = collection.has(name)
        ? collection.get(name)
        : new Subject<Events<T>>();
    
    if (subject !== undefined) {
        collection.set(name, subject); 
        /* ^ Type '{ type: "a"; payload: string; }' is not assignable 
           to type 'Events<T>'.
         */
        const subscription = subject.subscribe(cb)
        /* ^ This expression is not callable. Each member of the union 
           type '{ (observer?: Partial<Observer<Events<T>>> | undefined): 
           Subscription; (next: (value: Events<T>) => void): Subscription; 
           ... 1 more ...' has signatures, but none of those signatures 
           are compatible with each other.
         */
        return subscription;
    }

    return null;
}

fnWithConstraint("b", (event) => console.log(event));
// expect typeof event -> { type: "b"; payload: number; }

我无法让它编译成功。我链接到的 TS Playground 显示了我在 line:45 上获得的正确结果,但编译器抱怨 Subjects 类型签名,我似乎无法解决它。我错过了什么?

你只需要重载你的函数:

import { Subject, Subscription } from "rxjs";

const EventNames = {
  a: "a",
  b: "b",
  c: "c",
} as const;

type Payloads = {
  [EventNames.a]: string;
  [EventNames.b]: number;
  [EventNames.c]: boolean;
};

type EventTypes = keyof typeof EventNames;

// all possible event objects
type Events<T extends EventTypes> = { type: T, payload: Payloads[T] }

// all possible callbacks
type Callback<T extends EventTypes> = (event: Events<T>) => void

// all possible subjects
type Subjects<T extends EventTypes> = Subject<Events<T>>

const collection = new Map<EventTypes, Subjects<EventTypes>>();

function fnWithConstraint<T extends EventTypes>(
  name: T,
  cb: Callback<T>
): Subscription | null
function fnWithConstraint(
  name: EventTypes,
  cb: Callback<EventTypes>
): Subscription | null {
  const subject = collection.has(name)
    ? collection.get(name)
    : new Subject<Events<EventTypes>>()

  if (subject !== undefined) {
    collection.set(name, subject);
    return subject.subscribe(cb)
  }
  return null

}

fnWithConstraint("b", (event) => console.log(event));

Playground

在这种情况下,您应该放松一点。从外部来看,这个函数现在是安全的。

您的示例不起作用,因为您使用的是子类型 T。它并不意味着 T 等于 a | b | c 它只是意味着 T 可能是这个联合的任何子类型。

考虑这个例子:

function fnWithConstraint<T extends EventTypes,>(
  name: T,
  cb: Callback<T>
): Subscription | null {
  const subject = collection.has(name)
    ? collection.get(name)
    : new Subject<Events<T>>()

  if (subject !== undefined) {
    collection.set(name, subject);
    return subject.subscribe(cb)
  }
  return null
}

declare const a:'a' & {__tag:'Hello'}

fnWithConstraint(a, (event) => console.log(event));

event 参数是 Events<"a" & { __tag: 'Hello'; }>.

虽然 TS 允许您使用 const a,但使用 event 参数是不安全的,fnWithConstraint(a, (event) => console.log(event.type.__tag /** Hello */));

请记住,a for typescript 是一个常规对象,可以有自己的子类型。看到这个:keyof 'a'。超过 30 个属性。

TS 还将允许您将此品牌类型与我的解决方案一起使用,但 event 参数将是 valid/safe


can class methods be overloaded in the same way

是的。

简单示例:

class Foo {
  run(arg: string): number
  run(arg: number): string
  run() {
    return 'NOT IMPLEMENTED' as any

  }
}

const result = new Foo();
result.run(42) // string
result.run('str') // number

Playground

Official overloads documentation