typescript-eslint icon indicating copy to clipboard operation
typescript-eslint copied to clipboard

Rule proposal: no-unsafe-typeof-function

Open kirkwaiblinger opened this issue 9 months ago • 3 comments

Before You File a Proposal Please Confirm You Have Done The Following...

My proposal is suitable for this project

  • [X] My proposal specifically checks TypeScript syntax, or it proposes a check that requires type information to be accurate.
  • [X] My proposal is not a "formatting rule"; meaning it does not just enforce how code is formatted (whitespace, brace placement, etc).
  • [X] I believe my proposal would be useful to the broader TypeScript community (meaning it is not a niche proposal).

Description

Unintuitively, TS narrows typeof x === 'function' to the unsafe Function type if x is not already constrained. However, it is safe to use on a constrained type. We should report on unconstrained callables.

Fail Cases

declare const maybeFunction: unknown;
if (typeof maybeFunction === 'function') {
    // TS allows this.
    maybeFunction('call', 'with', 'any', 'args');
}

Pass Cases

declare const maybeFunction: string | (() => string);
if (typeof maybeFunction === 'function') {
    // TS errors if more args are provided than the declared call signature, so this is safe.
    maybeFunction();
}

Additional Info

it's probably fine/good to allow typeof x === 'function as long as x isn't called. I'm not sure exactly where the line should be drawn between reporting and not reporting. It seems like it should be reported if we pass x to a function that might expect a specific call signature. Maybe a heuristic could be typeof x === 'function is ok as long as x isn't referenced in the true branch of the conditional?

kirkwaiblinger avatar May 16 '24 16:05 kirkwaiblinger

This would probably be better as a part of no-unsafe-call. Eg ban all calls of a Function typed value, just like it bans all calls of an any typed values

bradzacher avatar May 16 '24 20:05 bradzacher

I think I'm ok with that, too. After a little experimentation i think that's safe as far as passing the narrowed function, too, since

declare const looselyTypedFunction: Function;
// TS error, not assignable.
const strictlyTypedFunction: () => void = looselyTypedFunction;

// ditto for passing as a parameter to functions
declare function callsWithAConcreteCallSignature(f: () => void): void;
// TS error
callsWithAConcreteCallSignature(looselyTypedFunction)

So I think Function is only unsafe when directly called, which would get caught with no-unsafe-call

kirkwaiblinger avatar May 16 '24 20:05 kirkwaiblinger

I guess the downside is just that I could see wanting no-unsafe-call disabled but still wanting the sneaky Function call safety

kirkwaiblinger avatar May 16 '24 21:05 kirkwaiblinger