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

2

7

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.

Rutherford answered 9/2, 2019 at 23:36 Comment(3)
isIterable(obj: unknown): obj is Iterable<unknown>?Muddler
@Muddler It’s not legal to do anything with an unknown without a type assertion. It's also not legal to access an arbitrary property on object without turning on suppressImplicitAnyIndexErrors; and even then you're not allowed to do obj.foo, and a linter will complain about obj['foo']. (It will work for obj[Symbol.whatever], which is a workaround for the above situation, but not for others where I want to narrow the type by a string-named key, i.e. 'then' for promises.)Rutherford
Yeah, I see what you're talking about now.Muddler
M
6

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!

Muddler answered 10/2, 2019 at 3:14 Comment(1)
honestly the safe widening step is probably what eluded me here, thanks!Rutherford
C
1

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;
Como answered 10/2, 2019 at 22:46 Comment(2)
I want to dodge as assertions as much as possible; found out you can accomplish that by using something like type Unknown<T> = {[k in keyof T]?: unknown};. As @Muddler clued me in, it's legal to assign anything to it without a type assertion, and it accurately reflects what you know about the object - it may or may not have those properties, and you don't know what their types are if it does have them.Rutherford
Sadly neither will work right if you're looking for a Symbol-keyed property because mapped types don't go over those for some reason.Rutherford

© 2022 - 2024 — McMap. All rights reserved.