Multiple type predicates in Typescript
Asked Answered
L

3

6

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
  }
}
Langston answered 3/11, 2021 at 20:5 Comment(0)
D
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

Denote answered 3/11, 2021 at 20:12 Comment(4)
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.Inconveniency
Thanks! I added a bit more detail to the question. This solution would work, but involves some seemingly redundant workLangston
@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.Denote
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>Langston
W
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[]
}
Wheeler answered 3/5, 2024 at 23:16 Comment(0)
H
0

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 }

Humiliation answered 23/10, 2024 at 13:43 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.