7

I have a case where I'm frequently checking both the value of a boolean and another union type together. While they're logically connected and could hypothetically just check one, I need to check both so the TS compiler knows both types.

let multiple: boolean
let value: Array<string> | string 
...
if (multiple && isArray(value)) {
  // TS knows multiple === true and value is an Array
}

Is there a way to write a type checking function where the type predicate asserts multiple values?

Something like this:

// Not valid Typescript
function isMulti (
  multi: boolean, 
  value: Array<string> | string
): (multi is true) && (value is Array<string>) {
  return multi && isArray(value)
}

In most cases I can get by with just checking isArray, but there are cases where I want to reuse a check like the one above. Is this possible?


In response to @kaya3:

To give a more accurate use case, I have something similar to the following:

I could assign multiple and value to an object with a DiscriminatedUnion type, but that adds more complexity than I think this needs.

type ValueType<M extends boolean> = M extends true ? Array<string> : string;

interface PropTypes<M extends boolean> {
  multiple: M,
  // more properties
}

type DiscriminatedUnion = {
  multiple: true,
  value: Array<string>
} | {
  multiple: false,
  value: string
}

function myFunction<M extends boolean>({
  multiple
}: PropTypes<M>) {

  let value: ValueType<M>;

  // ... later

  if (multiple) {
    // TS will not assert the type of `value` here
  }

  // Trying discriminated union
  // TS Error: Type '{ multiple: boolean; value: string | string[]; }' is not assignable to type '{ multiple: false; value: string; }'
  let obj: DiscriminatedUnion = {
    multiple,
    value
  }
}

3 Answers 3

7

It isn't currently possible to solve your problem this way; @jcalz helpfully notes that there is an open feature request for it. That said, your problem is probably better solved in a different way.

For pretty much any question like "I have these two values, how can I tell Typescript their types are related?", the answer is to put them together as properties in an object. Then you can make the object's type a discriminated union, so that a test on one of the properties can narrow the type of the whole object.

type DiscriminatedUnion =
    | {multiple: false, value: string}
    | {multiple: true, value: string[]}

declare let obj: DiscriminatedUnion;

if(obj.multiple) {
    // here, obj.value is narrowed to string[]
    obj.value
}

Playground Link

Sign up to request clarification or add additional context in comments.

4 Comments

Yeah, the question is ostensibly about github.com/microsoft/TypeScript/issues/26916 or the like, but the actual use case is asking for discriminated unions.
Thanks! I added a bit more detail to the question. This solution would work, but involves some seemingly redundant work
@AdamMThompson Well yes, your edit to the question still has two separate values in different variables, instead of one object. You also didn't actually use a discriminated union type.
Sorry, should've added more there. I tried using a union there, but the TS compiler still complains since value could be either string | Array<string>
1

With typescript you sometimes need to forego deconstructing objects until they can be properly discriminated. It often demands rethinking the flow. If you don't deconstruct the props right away, it can properly infer the type:

type Props = {
    value: string[],
    multiple: true
} | {
    value: string,
    multiple: false
}

function doSomethingAndDiscriminate(props:Props) {
    if(props.multiple) {
        const value = props.value[0]
        return {somethingNew: value}
    } else {
        const value = props.value
        return {somethingNew: value}
    }
}

Playground link

In this example it properly infers the return type of both possibilities to be { somethingNew: string }

Comments

0

This is slightly old but I was looking for similar functionality where discriminated unions were not a good fit, and ended up using something similar to the following.

let multiple: boolean;
let value: string | string[];

// The wrapper type for the variables that need to be checked.
type Wrapper = {
    multiple: boolean;
    value: string | string[];
};

// The expected type.
type Multiple = {
    multiple: true;
    value: string[];
};

// The function that will test if the fields in the wrapped type are the expected types.
function isMulti(wrapper: Wrapper): wrapper is Multiple {
    return wrapper.multiple && Array.isArray(wrapper.value);
}

// Code that sets multiple and value...

// Test the variable types.
const args: Wrapper = {
    multiple,
    value
};

if (isMulti(args)) {
    args.multiple; // true
    args.value; // string[]
}

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.