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