3

If I have the following union which wraps a function and its argument, how can I call it?

type Wrapper = {
  fn: (a: string) => void
  arg: string
} | {
  fn: (a: number) => void
  arg: number
}

let foo!: Wrapper
foo.fn(foo.arg) // Error Type 'string' is not assignable to type 'never'.

I can't figure out how to call it. Everything that I've tried essentially boils down to a cast, (e.g. casting to (a:any) => void) which I can do if I have to, but I feel that I shouldn't have to do that.

Can this function be called without casting?

Edit: To clarify, I'm asking if there exist solutions which don't involve changing definition of Wrapper.

3
  • 2
    The problem is that, given type F = (x: A) => T and type G = (x: B) => U, the union type H = F | G is (x: A & B) => T | U. To call a function of type H, we would need supply an argument that is a subtype of both A and B. Commented Sep 21, 2020 at 16:44
  • @AluanHaddad Yes, that seems to be typescript's logic, but it clearly loses type information. I want to know if there is a way not to lose the information about the relationship between fn and arg. Commented Sep 21, 2020 at 16:53
  • The language doesn't consider arg to be a discriminant. You would have to introduce a discriminant i.e. Playground Link Commented Sep 21, 2020 at 17:45

2 Answers 2

1

You can use a conditional type to disambiguate the types. Unfortunately it requires you basically duplicate your wrapper union types again and if you have more things you need to check, your conditional type will get pretty ugly. But it does let you avoid a cast (sort of).

type Wrapper = {
  fn: (a: string) => void
  arg: string
} | {
  fn: (a: number) => void
  arg: number
}

type Disambiguate<W extends Wrapper> = W['arg'] extends string
  ? { fn: (a : string) => void, arg: string }
  : { fn: (a: number) => void, arg: number };

let foo!: Disambiguate<Wrapper>

// The compiler is satisfied. No error here.
// foo can only be one of the branches in the conditional type.
foo.fn(foo.arg);
Sign up to request clarification or add additional context in comments.

Comments

0

Not exactly sure about your use case, but above can be achieved by using this:


type ArgType = number | string

type Wrapper<T extends ArgType> = {
  fn: (a: T) => void
  arg: T
}

let foo!: Wrapper<number>
foo.fn(foo.arg)

let foo1!: Wrapper<string>
foo1.fn(foo1.arg)

1 Comment

Thanks for the attempt. My example was simplified. I'm looking for an answer to the general case when we can't move the union out in this way.

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.