1

I'm playing around with some logic to create a strongly typed worker, which should be able to accept only a pre-defined set of messages, and respond with the appropriate response type given the message.

Something like the following

type MsgStruct<T, P, R> = {
  type: T
  payload: P
  response: R
}

type FooMsg = MsgStruct<'foo', number, number>
type BarMsg = MsgStruct<'bar', boolean, string>
type BazMsg = MsgStruct<'baz', string, boolean[]>

// Messages that can be handled by the worker
type WorkerMsg =
  | FooMsg
  | BarMsg
  | BazMsg

// Creation logic
type StronglyTypedWorker<T> = T extends { type: infer A, payload: infer B, response: infer C } ? {
  postMessage: (msg: { type: A, payload: B }) => Promise<C>
} : never

declare const worker: Worker
declare const createWorker: <T extends MsgStruct<unknown, unknown, unknown>>(worker: Worker) => StronglyTypedWorker<T>

// Example
const strongWorker = createWorker<WorkerMsg>(worker)

declare const foo: FooMsg

const fooRes = strongWorker.postMessage(foo)
  .then(res => {}) // expecting `res` to be of type `number` here

This doesn't work, as tsc complains with

Argument of type 'FooMsg' is not assignable to parameter of type 'never'. The intersection '{ type: "foo"; payload: number; } & { type: "bar"; payload: boolean; } & { type: "baz"; payload: string; }' was reduced to 'never' because property 'type' has conflicting types in some constituents.ts(2345)

Is there a better way to do this? Or rather, one that works?

bugs
  • 14,631
  • 5
  • 48
  • 52

1 Answers1

1

You were almost there.

The problem was in createWorker return type, it was a union of functions.

When you want to call union of functions, their arguments will intersect/merge. Because function arguments are in contravariant positions. Thet's why you got never type.

You can read more about it in my article

All you need to do is to produce function overloading, in other words you should derive intersection of functions instead of union:

type MsgStruct<T, P, R> = {
    type: T
    payload: P
    response: R
}

type FooMsg = MsgStruct<'foo', number, number>
type BarMsg = MsgStruct<'bar', boolean, string>
type BazMsg = MsgStruct<'baz', string, boolean[]>

// Messages that can be handled by the worker
type WorkerMsg =
    | FooMsg
    | BarMsg
    | BazMsg

// Creation logic
type StronglyTypedWorker<T> = T extends { type: infer A, payload: infer B, response: infer C } ? {
    postMessage: (msg: { type: A, payload: B }) => Promise<C>
} : never

declare const worker: Worker

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

declare const createWorker: <T extends MsgStruct<unknown, unknown,unknown>>(worker: Worker) => UnionToIntersection<StronglyTypedWorker<T>>

// Example
const strongWorker = createWorker<WorkerMsg>(worker)

declare const foo: FooMsg

const fooRes = strongWorker.postMessage(foo)
    .then(res => { }) // res is number

Playground

Btw, most of the time you don't need a union of functions