Home > OS >  Typescript make parameters depend on a parameter that's a union of literals
Typescript make parameters depend on a parameter that's a union of literals

Time:07-26

There a few other questions about this, but I can't find one for this exact use case.

I have a method signature like this:

function respondToAction(action: "create" | "update" | "delete", old: Record<string, any> | undefined, change: Record<string, any> | undefined)

I want to express the typings in a way that if action === "create", old is inferred as undefined, if action === update, both old and change are defined, and if action === delete, then change is undefined.

Is there a way I can express this?

edit: I just tried this:

type Action = "create" | "update" | "delete";
type Function =
 <A extends Action>(action: A,
  old: A extends "create" ? undefined : Fields,
  change: A extends "create" | "update" ? Fields : undefined
) => void

but it doesn't seem to work. Within the function, I tried to branch on action, and even if I have a if(action === "create") { ... } it doesn't properly infer change as defined within the condition.

CodePudding user response:

You can give respondToAction a rest parameter whose type is a discriminated union of rest tuple types, and immediately destructure into the three variables named action, old, and change:

type Data = Record<string, any>;

function respondToAction(...[action, old, change]:
    [action: "create", old: undefined, change: Data] |
    [action: "update", old: Data, change: Data] |
    [action: "delete", old: Data, change?: undefined]
) { /* impl */ }

You can see that this behaves as desired from the caller's side. It behaves similarly to overloads, but you have only one actual call signature:

respondToAction("create", undefined, { a: 1, b: 2 });  // okay
respondToAction("update", { a: 1, b: 2 }, { a: 1, b: 3 }); // okay
respondToAction("delete", { a: 1, b: 3 }); // okay
respondToAction("delete", undefined, { a: 1, b: 2 });  // error
respondToAction("create", { a: 1, b: 2 }, { a: 1, b: 3 }); // error
respondToAction("update", { a: 1, b: 3 }); // error

And since TypeScript 4.6 and above supports control flow analysis for destructured discriminated unions, you can switch on action in the implementation and the compiler will automatically narrow old and change accordingly:

function respondToAction(...[action, old, change]:
    [action: "create", old: undefined, change: Data] |
    [action: "update", old: Data, change: Data] |
    [action: "delete", old: Data, change?: undefined]
) {

    old.testProp; // compiler error, possibly undefined
    change.testProp; // compiler error, possibly undefined

    if (action !== "create") {
        old.testProp // okay, old is known to be defined
    }
    if (action !== "delete") {
        change.testProp // okay, change is known to be defined    
    }
    
}

Playground link to code

CodePudding user response:

Yes, you can use functions overload

Example:

function respondToAction(action: "create"): void;
function respondToAction(action: "delete", old: Record<string, any>): void;
function respondToAction(action: "update", old: Record<string, any>, change: Record<string, any>): void;
function respondToAction(action: "create" | "update" | "delete", old?: Record<string, any>, change?: Record<string, any>): void {
    // TODO
}

Playground

  • Related