Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 661
millimoose Avatar asked Feb 09 '19 23:02

millimoose


People also ask

How do I know my TS type?

Use the typeof operator to check the type of a variable in TypeScript, e.g. if (typeof myVar === 'string') {} . The typeof operator returns a string that indicates the type of the value and can be used as a type guard in TypeScript.

What is ?: In TypeScript?

Using ?: with undefined as type definition While there are no errors with this interface definition, it is inferred the property value could undefined without explicitly defining the property type as undefined . In case the middleName property doesn't get a value, by default, its value will be undefined .

What is a type predicate?

A type predicate is a specially-defined function that returns a boolean when a specified argument returns true.


2 Answers

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!

like image 96
jcalz Avatar answered Sep 23 '22 10:09

jcalz


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;
like image 29
Josh Avatar answered Sep 22 '22 10:09

Josh