1

Based on this solution, I've created a type guard:

const isNoStringIterable = (obj: any): obj is Iterable<any> => {
  if (obj == null) return false;
  else if (typeof obj === "string") return false; // this isn't properly reflected by the type guard
  else return typeof obj[Symbol.iterator] === "function";
};

The main difference to the linked solution is that I want it to return false in case we have a string. Is there any way to narrow down the type guard declaration to something like "obj is Iterable but no string"?

NotX
  • 1,516
  • 1
  • 14
  • 28
  • 1
    Could you show how you intend to call `isNoStringIterable()`? Depending on the use case and what you need to see happen when it returns `false` you might have different implementations. [This version](https://tsplay.dev/W4jqvN) is generic and filters the input type if it's a union. If that works for you I could write up an answer explaining; if not, what am I missing? – jcalz Mar 09 '23 at 14:02
  • @jcalz: Thanks for your response! As background: I wanted check the type of React `children` and planned to map them in case it's an `Iterable` but not a `string`. Meanwhile, I found out that I'm probably better off with `React.Children.toArray()`, but I created this question anyway out of curiosity. I'll gladly accept your solution as an answer, thanks for the insights! – NotX Mar 09 '23 at 14:08

1 Answers1

1

TypeScript doesn't have negated types as implemented (but never merged/released) in microsoft/TypeScript#29317, so there's no direct way to say not string and thus no direct way to say Iterable<any> & not string. Instead we need to work around it by expressing "not" in some other fashion.

If your input type is a union, then you can filter it to include Iterable<any>-compatible members but exclude string-compatible members. Union filtering isn't exactly the same as type negation, but it often serves the same purpose.

There are utility types like Extract<T, U> and Exclude<T, U> you can use to do this, but you also can write your own via a distributive conditional type directly (which is how Extract<T, U> and Exclude<T, U> are implemented):

type NoStringIterable<T> =
  T extends string ? never : T extends Iterable<any> ? T : never;

You can test that this behaves as expected:

type Test = NoStringIterable<string | number[] | Set<boolean> | Date>;
// type Test = Set<boolean> | number[]

Both Set<boolean> and number[] are Iterable<any> and not string, so that's what comes out. And now we can make isNoStringIterable a generic function where its input is of type T and its output is obj is NoStringIterable<T>:

const isNoStringIterable = <T,>(obj: T): obj is NoStringIterable<T> => {
  if (obj == null) return false;
  else if (typeof obj === "string") return false;
  else return typeof (obj as any)[Symbol.iterator] === "function";
};

Let's test it out:

function foo(obj: string | number[]) {
  if (isNoStringIterable(obj)) {
    obj.map(x => x + 1);
  } else {
    obj.toUpperCase();
  }
}

Looks good. The compiler narrows string | number[] to number[] in the true block and to string in the false block, as expected.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360