'Typescript a function returns union type isn't compatible with union of functions

Sorry maybe my question title isn't accurate, and I know a function return union is not equivalent to union of functions.

But my case is simple enough and I think it should be correct.

type Functions = (() => 'a') | (() => 'b')

type Returns = 'a' | 'b'

const buildFunction = (returnValue: Returns): Functions => () => returnValue

Because the input is one of 'a' | 'b', so the built function should also be one of (() => 'a') | (() => 'b').

However there is an error:

Type '() => Returns' is not assignable to type 'Functions'.
  Type '() => Returns' is not assignable to type '() => "a"'.
    Type 'Returns' is not assignable to type '"a"'.
      Type '"b"' is not assignable to type '"a"'.

Update:

Thanks @jsejcksn, your solution is great for the above scenario. But my actual scenario is more complex than it, and this solution doesn't seem to solve my problem. Here is what I am facing on:

TS playground

type MyEvent = { type: 'a' } | { type: 'b' }

type WithToJson<T> = T extends unknown
  ? T & { toJSON: () => T }
  : never
// will be { type: 'a'; toJSON: () => { type: 'a' } } | { type: 'b'; toJSON: () => { type: 'b' } }

// adding a `toJSON` method to each event
const addToJson = (event: MyEvent): WithToJson<MyEvent> => ({
  ...event,
  toJSON: () => event  // error
})

// try to use generic function to restrict type, but doesn't work
const addToJsonGeneric1 = <TEvent extends MyEvent>(event: TEvent): WithToJson<MyEvent> => ({
  ...event,
  toJSON: () => event
})

// give the generic type to WithToJson type
const addToJsonGeneric2 = <TEvent extends MyEvent>(event: TEvent): WithToJson<TEvent> => ({
  ...event,
  toJSON: () => event
})


Solution 1:[1]

Update in response to your comment and updated question:

I now think that this is what you're looking for: a function for assigning a JSON transformer function to an object:

TS Playground

type Json = boolean | null | number | string | Json[] | { [key: string]: Json };
type Serializer<T, Serializable extends Json = Json> = (value: T) => Serializable;
type Producer<T> = () => T;
type ToJSONMethod<T> = { toJSON: Producer<T> };
type WithCustomJSON<T extends object, Serializable extends Json> = T & ToJSONMethod<Serializable>;


function makeSerializable <T extends object>(o: T): asserts o is T & ToJSONMethod<T>;
function makeSerializable <
  T extends object,
  Serializable extends Json,
>(o: T, serializer: Serializer<T, Serializable>): void;
function makeSerializable <
  T extends object,
  Serializable extends Json,
>(o: T, serializer?: Serializer<T, Serializable>): void {
  Object.defineProperty(o, 'toJSON', {
    configurable: true,
    enumerable: false,
    writable: true,
    value: serializer ? () => serializer(o) : () => o,
  });
}


// Usage example:

const evA = { type: 'a' as const };

makeSerializable(evA, ev => ({type: ev.type, message: 'This was serialized!'}));
evA.toJSON(); /* ?
    ~~~~~~
Property 'toJSON' does not exist on type '{ type: "a"; }'.(2339) */

console.log(Object.keys(evA)); // ["type"]
console.log(JSON.stringify(evA)); // {"type":"a","message":"This was serialized!"}

makeSerializable(evA);
evA.toJSON(); // ok ?

console.log(Object.keys(evA)); // ["type"]
console.log(JSON.stringify(evA)); // {"type":"a"}


Original answer:

If you are trying to restrict the types of arguments that can be provided to the function creator, then you can do it using a generic like this:

TS Playground

type ReturnValue = 'a' | 'b';

const buildFunction = <R extends ReturnValue>(value: R) => (): R => value;

const fnA = buildFunction('a'); // () => "a"
const fnB = buildFunction('b'); // () => "b"
const a = fnA(); // "a"
const b = fnB(); // "b"

const fnC = buildFunction('c'); /*
                          ~~~
Argument of type '"c"' is not assignable to parameter of type 'ReturnValue'.(2345) */

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1