millimoose
millimoose

Reputation: 39950

How to write type guards with no-unsafe-any enabled?

I'm trying to tighten up my TS code by using a stricter lint ruleset, but I'm struggling with what should be legitimate uses of dynamism.

I'm making a type guard to detect if something is iterable (to wrap it in an array if not), and I have no idea what to tell TS other than suppressing the lint rule to tell this is kosher:

function isIterable(obj: any): obj is Iterable<unknown> {
    return obj && typeof obj[Symbol.iterator] === 'function';
}

I tried changing this to:

function isIterable(obj: undefined | {[Symbol.iterator]?: unknown}): obj is Iterable<unknown> {
    return !!obj && typeof obj[Symbol.iterator] === 'function';
}

which compiles without using any, but it's not useful, because I want to pass values of unknown type to it.

Is there a "clean" way of saying "yes I actually want to rely on JS returning undefined for accessing a property that doesn't exist on an object"? Esp. since that's kind of whole point of writing type guards.

Upvotes: 7

Views: 2194

Answers (2)

Josh
Josh

Reputation: 3420

Another good strategy is to use Partial with an as cast.

interface RegularForm {
    regular: number;
}

interface FancyForm extends RegularForm {
    fancy: string;
}

const isFancyForm = (instance: RegularForm): instance is FancyForm =>
    (instance as Partial<FancyForm>).fancy !== undefined;

Upvotes: 1

jcalz
jcalz

Reputation: 327819

I don't know if something like no-unsafe-any buys you too much inside the implementation of a user-defined type guard, since usually the whole point of such a type guard is to allow the compiler to narrow values it can't normally do through the built-in control-flow narrowing. I'd certainly understand suspending a linter rule inside such an implementation.

But I think you can get nearly the behavior you're looking for like this:

function isIterable(obj: unknown): obj is Iterable<unknown> {
  if ((typeof obj !== 'object') || (obj === null)) return false; 
  // obj is now type object
  const wObj: { [Symbol.iterator]?: unknown } = obj; // safely widen to wObj
  return typeof wObj[Symbol.iterator] === 'function'; 
}

That's a few hoops to jump through, but the idea is to use control flow narrowing to narrow unknown to object, then widen object specifically to a type with an optional property you're trying to check (this happens by introducing a new variable). And finally, check the type of that property on the widened type. Since the property key you're checking is a symbol type, you need to mention the particular property name in the widened type. If the property key is a string, you can get away with using a string index signature:

function isPromise(obj: unknown): obj is Promise<unknown> {
  if ((typeof obj !== 'object') || (obj === null)) return false;
  // obj is now type object
  const wObj: {[k: string]: unknown} = obj; // safely widen to wObj
  return typeof wObj.then === 'function';
}

Anyway, I hope that gets you closer to your goal. Good luck!

Upvotes: 6

Related Questions