1
votes

I want to create a core dialog class, so that when adding a new dialog, TS will resolve a dialog type (and it's return types) automatically, basing on the input given. I was able to achieve most of it, but I failed when it comes to the return values.

Each dialog return a Promise, promise result should base on the dialog type passed, eg.

  • when T is PromptDialogOptions return Promise<string | number>,
  • when T is ConfirmDialogOptions return Promise<boolean>,
  • whereas when T is MessageDialogOptions then return Promise<void>.

My actual code for creating a dialog (I have marked the lines that produce errors and explained them below due to the length):

let dialogs: DialogOptions[] = [];

newDialog<T extends DialogOptions, R extends InnerDialogType<T>>(dialog: T) : Promise<R> => {
    const promise = new Promise<R>(res => {
        // Set dialog resolver
        dialog.resolver = res; // error #1 (see below)
    });

    // Create dialog close handler
    dialog.closeHandler = (result: R) => { // error #2 (see below)
        // Resolve a promise
        dialog.resolver(result);
        // Close the dialog
        // this.closeDialog(dialog);
    };

    // Store dialog
    dialogs = [...dialogs, dialog];

    return promise;
}

This code produces two errors:

  • #1 line dialog.resolver = res;

Type '(value?: R | PromiseLike | undefined) => void' is not assignable to type '((value?: void | undefined) => void) | ((value?: string | number | undefined) => void) | ((value?: boolean | undefined) => void) | undefined'. Type '(value?: R | PromiseLike | undefined) => void' is not assignable to type '(value?: void | undefined) => void'.

  • #2 line dialog.closeHandler = (result: R) => {

Type '(result: R) => void' is not assignable to type '((result: void) => void) | ((result: string | number) => void) | ((result: boolean) => void)'.

There's clearly an issue with the wrong types being used for handling result in the BaseDialog.

Question

How can I make BaseDialog.resolver and BaseDialog.closeHandler accept the generic type of R, that is inferred depending on the dialog result type being passed?

Some examples of what I want to achieve:

const confirmDialog : ConfirmDialogOptions = {
   title: "Hello",
   message: "I'm stuck. Will you help?",
   type: DialogType.DIALOG_CONFIRM
};

newDialog(confirmDialog); 
  • Expected result: Promise<boolean>
const dialog = {
    title: "Invalid",
    message: "Invalid dialog is bad!",
    type: DialogType.DIALOG_MESSAGE
}

newDialog(dialog);
  • Expected result: error, since dialog doesn't inherit from BaseDialog
const promptDialog : PromptDialogOptions = {
   title: "Hello",
   message: "Say hello",
   maxLength: 10,
   type: DialogType.DIALOG_PROMPT
};

newDialog(promptDialog);
  • Expected result: Promise<string | number>

All types used

export const enum DialogType {
    DIALOG_MESSAGE,
    DIALOG_CONFIRM,
    DIALOG_PROMPT
}

export interface BaseDialog<T> {
    title: string;
    message: string;
    type: DialogType;
    resolver?: (value?: T) => void;
}

export interface MessageDialogOptions extends BaseDialog<void> { }

export interface ConfirmDialogOptions extends BaseDialog<boolean> { }

export interface PromptDialogOptions extends BaseDialog<string | number> {
    maxLength: number;
}

// Union dialogs
export type DialogOptions = MessageDialogOptions | PromptDialogOptions | ConfirmDialogOptions;
1

1 Answers

0
votes

Why not simply?

function newDialog<R>(dialog: BaseDialog<R>): Promise<R> {
   ...

This would make it clear to the compiler that R and T are for the same dialog subtype (which your current function declaration fails to express, causing the compiler error you mention).

If you chose the union type to enforce that the dialog is actually one of the known types, you can better accomplish this with overloads:

function newDialog(dialog: MessageDialog): Promise<void>;
function newDialog(dialog: ConfirmDialog): Promise<boolean>;
function newDialog(dialog: PromptDialog): Promise<string | number>;
function newDialog<R>(dialog: BaseDialog<R>): Promise<R> {
  // implementation here
}

BTW, as a caller, I'd probably prefer separate named methods to specifying the type with an enum:

newConfirmDialog({
  title: 'Really launch rockets?',
  text: 'Once launched, they can not be recalled.'
})