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.
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.
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 .
A type predicate is a specially-defined function that returns a boolean when a specified argument returns true.
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!
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With