0

I have a function that can receive two different object signatures as an argument. How do I type that?

i.e.

fn({ a: 'a', c: 'c' })
fn({ b: 2, c: 'c' })

however I tried to define the argument types, TS complains a and b do not exist on that type (only c which exists on both is fine)

export type IUploadImageArgs =
    | {
            uri: string;
            type: string;
            path: string;
      }
    | {
            path: string;
            file: any;
      };

const uploadImage = async ({ file, path }: IUploadImageArgs) => {

}
// Property 'file' does not exist on type 'IUploadImageArgs'

Side note: This is for a react native + web project, a function that abstracts the uploading of files, in which the web version accepts the file itself, while the native version accepts the local path of the file.

Thanks!

2
  • How about function fn(args:{a:string, c:string}|{b:number, c:string}) ? Commented May 21, 2021 at 11:11
  • I tried that, I'm getting the error a/b does not exist on the args (edited question to reflect that) Commented May 25, 2021 at 4:24

3 Answers 3

1

Like d2b said, in that simple case you can just have the parameter be a union type: function fn(obj: { a: string, c: string } | { b: number, c: string }).

If you have more complicated needs, like different return value for each set of arguments, you can declare the function multiple times. There needs to be a single implementation with a body and parameters that can accept any of the alternatives.

function fn(obj: { a: string, c: string }): number;
function fn(obj: { b: number, c: string }): string;
function fn(obj: { a: string, c: string } | { b: number, c: string }): number | string {
  // impl
}
Sign up to request clarification or add additional context in comments.

2 Comments

I tried that, I'm getting the error a/b does not exist on the args (edited question to reflect that)
You can't destructure a property on the argument object unless it exists on every member of the union type (in your case, path is safe to destructure but file is not). You should do const uploadImage = (args: IUploadImageArgs) => /* ... */ and then conditionally destructure args in the function body once you've narrowed its type.
0

One solution which allows you to destructure a union type like this directly (without first testing which branch of the union it is) is to give every branch the same property names, and make the "missing" properties on each branch optional of type undefined.

type ArgType =
    | {a: string, b: number, c?: undefined}
    | {a: string, b?: undefined, c: string}

function test({a, b}: ArgType): void {
    a //: string
    b //: number | undefined
}

Playground Link

This also means each of the non-shared properties acts as a discriminant for the union - without doing this, your code would accept an object like {a: 'foo', b: 23, c: 'bar'}, and it's not clear what the correct result should be. By discriminating the union in the above way, this mixed object would not be a valid argument.

Note that because Typescript doesn't keep track of relationships between different variables when doing control-flow type narrowing, if you did destructure {a, b, c} then a test to see if b is undefined would not narrow c to be of type string, and vice versa. For that, you should write arg: ArgType and destructure it after testing which branch of the union it is.

Comments

0

While the answers below cover some useful use cases, for my use case seems the only solution is to implement two different functions and call them based on the current platform, instead of having react native manage the file importing for me.

Thanks @joonas and @kaya3 for your answers!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.