Open dhmk083 opened 4 years ago
This check isn't safe if T
is instantiated with something like (x: string) => number
.
@DanielRosenwasser Why does the following alternative work then?
type Initializer<T> = T extends any ? (T | (() => T)) : never
function correct<T>(arg: Initializer<T>): T {
return typeof arg === 'function' ? arg() : arg // arg is callable in the true branch
}
arg
becomes Initializer<T> & Function
in the true branch and is callable. This example seems quite similar to me with the only difference, that Initializer<T>
from OP is a union function type, above not. We could ask the same question here: is the check for T
safe?
In addition, I expected Function
to be a very generous type. Today you already can do invocations with all sorts of arguments:
declare const foo: Function
// all compile
foo()
foo(3)
foo("bar",3, true)
So I would expect and wish that behavior at least gets consistent. Either for both examples, a) an error should be emitted or b) both should compile to be in line with the current Function
type behavior.
Appreciate your thoughts, thanks!
@bela53
const r2 = correct<(_: string) => number>((v: string) => v.toString().length)
this code passes type validation but blows in runtime.
@zerkms Good catch. You could fix that error by choosing a more narrow type relationship test in Initializer<T>
conditional type:
type Initializer<T> = T extends () => any ? (T | (() => T)) :
T extends Function ? never : (T | (() => T))
const r3 = correct<(_: string) => number>((v: string) => v.toString().length) // error
However, this isn't directly related to the main issue. typeof arg === 'function'
will only be able to narrow to Function
in the true branch, and in principle you can invoke Function
with any arguments.
The question remains, if both Initializer<T> & Function
(currently works) and (() => T) | (T & Function)
(currently doesn't work) are callable or not. Only one or the other would be inconsistent in my view.
that seems like a bug
Simplified.
function f<T>(arg: T & Function) {
typeof arg === 'function' && arg(); // `T & Function` is callable.
}
function g<T>(arg: T | (() => T)) {
typeof arg === 'function' && arg(); // Type 'T & Function' has no call signatures.(2349)
}
@bela53
type Initializer<T> = T extends Function ? never : (T | (() => T))
Seems to be working too and is shorter. I think I will use this typing.
By the way,
type Initializer<T> = T | (() => T)
// -or-
type Initializer<T> = T extends any ? (T | (() => T)) : never
const isFunction = (arg: any): arg is Function => typeof arg === 'function'
function correct_2<T>(arg: Initializer<T>): T {
return isFunction(arg) ? arg() : arg
}
const x = correct_2<() => number>(() => 5)
// x: () => number
This compiles, but produces a mismatch between type and runtime value.
Unfortunately, this typing doesn't always work. I've found some inconsistencies in its behavior:
// type Initializer<T> = T | (() => T)
// -or-
type Initializer<T> = T extends Function ? never : T | (() => T)
function f_1<T>(arg: Initializer<T>) {
return typeof arg === 'function' ? arg() : arg
}
function f_2<T extends number>(arg: Initializer<T>) {
return typeof arg === 'function' ? arg() : arg
}
function f_3<T>(arg: Initializer<Partial<T>>) {
return typeof arg === 'function' ? arg() : arg
}
function f_4<T>(arg: Initializer<Record<'a', T>>) {
return typeof arg === 'function' ? arg() : arg
}
If you try with line 1 typing, it will fail f_1 case as expected, but it will also work for cases 2,3,4.
If you try with line 3, it will fail for cases 2 and 3.
I don't get why it doesn't work for Partial
@falsandtru well summarized the issue. I would add one case:
// if you set a constraint for T, it works again (?)
function g2<T extends number>(arg: T | (() => T)) {
typeof arg === 'function' && arg(); // () => T is callable.
}
Plagyround with all three examples
@dhmk083 Your example seems to describe a different issue related to (unresolved) conditional types. Not sure why f_2
and f_3
aren't handled properly.
@bela53
In your last example T extends number
means that T is definitely not a Function
, so it works. If you replace it with T extends object
constraint, it will produce an error, because Function
extends object
, too.
Here is another simplified case:
// OK
function g<T>(arg: T extends Function ? never : (T | (() => T))) {
typeof arg === 'function' && arg(); // () => T is callable.
}
// error
function g2<T extends object>(arg: T extends Function ? never : (T | (() => T))) {
typeof arg === 'function' && arg(); // () => T is callable.
}
Adding a constraint breaks things.
Is this related #27422?
Ran into this while trying to use the SetStateAction<T>
type in react
:
type SetStateAction<S> = S | ((prevState: S) => S);
If using typeof foobar === 'function'
isn't the correct way to check whether we have a value, or a callable initializer function, what is?
Found that this reveals the issue:
function a<T>(b: T | (() => T)) {
return typeof b === 'function' ? b() : b
}
But this does not (and compiles file):
function a<T>(b: T | (() => T)) {
return b instanceof Function ? b() : b
}
Kind of odd, because typeof
checks seem to work normally. But this is a reasonable workaround for anyone who bumps into it. HT to @pbteja1998 :)
NOTE: this workaround can have problems in some situations: https://github.com/microsoft/TypeScript/issues/37663#issuecomment-856866935 Check a safer (though more verbose) workaround: https://github.com/microsoft/TypeScript/issues/37663#issuecomment-856961045)
FYI: instanceof
workaround doesn't work for values in generic Record
type Schema = {
id: string;
type: string;
}
type Config = {
schema: Schema | (() => Schema)
}
function f<T extends Record<string, Config>>(config: T, key: string) {
if (config[key] instanceof Function) {
config[key]() // This expression is not callable
}
}
Found that this reveals the issue:
function a<T>(b: T | (() => T)) { return typeof b === 'function' ? b() : b }
But this does not (and compiles file):
function a<T>(b: T | (() => T)) { return b instanceof Function ? b() : b }
Kind of odd, because
typeof
checks seem to work normally. But this is a reasonable workaround for anyone who bumps into it. HT to @pbteja1998 :)
Be careful with using x instanceof Function
(as suggested by @kentcdodds), as it won't be reliable in cases where the value comes from a different context (e.g. iframe/window/realm). Objects (including functions) from other contexts will have a different Function constructor in their prototype chain than the one being used to perform the check.
True, I should have mentioned that (I've definitely bumped into this issue in the past and it was very confusing at the time 😅). I'd love a solution rather than a workaround :)
Here's one that feels a little verbose, but seems to work:
type Fn = (...args: unknown[]) => unknown;
function getValue <
Provided,
T = Provided extends Fn ? ReturnType<Provided> : Provided,
>(valueOrFn: Provided): T {
return typeof valueOrFn === 'function' ? valueOrFn() : valueOrFn;
}
So basically at this point, the only way to make this work is to use instanceof
??
You know, I could have sworn this method of using typeof foo === 'function'
, used to work! In fact I just started noticing it in VSCode today.
Was this a regression?
It also doesn't seem to affect eslint...just weird
Aha! I knew I wasn't crazy! It seems this error only appears when I allow vscode to use the version of typescript which it comes bundled with: 4.6.0-dev.20220124
. Once I switch to the typescript version installed locally (4.5.5), the error disappears.
Most likely cause is that some feature in the development versions of typescript is not included in the later stable versions, and that is what has been triggering this same error since 2020. From 3.9 nightly till 4.6 nightly
This is the only reasonable way I could find around this without having to cast all the things as <shape of function>
:
const isFunction = (x: unknown): x is Function => typeof x === 'function'
Here's an example of it in action with a nice cond
function I've been working on
I've been using this type guard recently, and it also seems to solve the problem. It would be nice to have a fix, though!
declare type AnyFunction = (...args: unknown[]) => unknown
function isFunction<T extends AnyFunction>(value: unknown): value is T {
return typeof value === 'function'
}
This cropped up for me today, after updating to v4.6.x from v4.5.5. I'd love to know what exactly changed between these two versions that broke my code. From the above, nobody has really figured out what the root cause might be. If the difference is so simply described (and by that I do not mean so simple a problem) in https://github.com/microsoft/TypeScript/issues/37663#issuecomment-605846042, what's the hold up on a triage here?
@WoodyWoodsta this bug is fully triaged. What do you mean?
@RyanCavanaugh Might be a difference in our understanding on what "triage" means :) Not sure how the backlog milestone is treated for this project but given this was identified and added to that backlog two years ago, I was wondering if there was anything holding it back given it has now clearly found its way into stable release.
i think maybe typeof == 'function'
cannot exclude unknown type?
type Initializer<T> = T | (() => T)
function correct<T>(arg: Initializer<T>) {
return typeof arg === 'object' && typeof arg == "function" ? arg() : arg // no error
}
type NoUnknown = number|string|never;
function correct2<T extends NoUnknown>(arg: Initializer<T>) {
return typeof arg === 'function' ? arg() : arg // no error
}
type withUnknown = number|string|never|unknown;
function correct3<T extends withUnknown>(arg: Initializer<T>) {
return typeof arg === 'function' ? arg() : arg // same error
}
I gave up and just casted it.
// global.d.ts
type Fn<TArgs extends ReadonlyArray<any>=unknown[], TRet=unknown> = (...args: TArgs[]) => TRet
type AnyFn = Fn<any[],any>
// resolvable.ts
export type Resolvable<T = any, TArgs extends ReadonlyArray<any> = []> = T | ((...args: TArgs) => T)
export type Resolved<T> = T extends Fn ? ReturnType<T> : T
export function resolveValue<T, TArgs extends ReadonlyArray<any>>(val: Resolvable<T, TArgs>, ...args: TArgs): T {
return typeof val === 'function' ? (val as AnyFn)(...args) : val
}
I don't think this is a bug. You use typeof arg
to identify whether the arg is a function, but from the perspective of the TS compiler, arg
may not be a function as you declared T | (() => T)
. The TS compiler doesn't know what your code does, therefore it is not safe to call arg()
I think a safer approach would be to let the developer decide what is or isn't type T
using a function that uses a type predicate
function someFuncName<T>(arg: T | (() => T), isTypeT: (val: unknown) => val is T): T {
if (isTypeT(arg)) {
return arg;
}
return arg();
}
Got more workarounds! lol. Like @EltonLobo07 mentioned, I'm using a predicate to get the job done. But for my code base, the following is sufficient for me to work around the issue.
type MyUnion<T> = T | ((arg: string) => T);
function doMyLogic<S>(arg: MyUnion<S>): void {
const stringArg = "my-string";
// @ts-expect-error -- Weird TypeScript Bug
const value = typeof arg === "function" ? arg(stringArg) : arg;
const betterValue = isFunction(arg) ? arg(stringArg) : arg; // Success
}
function isFunction(value: unknown): value is (arg: string) => unknown {
return typeof value === "function";
}
This is obviously not reliable for broad usage. But since I needed a quick TS fix and the impact was minimal, I reached for this. Less verbosity. The narrowed type in the doMyLogic
function uses the correct return value (rather than unknown
).
This works fine? Or maybe I missed the point...?
export type ValueTFn<T = any> = T extends ((...args: unknown[]) => unknown) ? never : T | (() => T);
export namespace ValueTFn {
export function get<T = any>(valueOrFn: ValueTFn<T>): T {
return typeof valueOrFn === "function" ? valueOrFn() : valueOrFn;
}
}
Compiles, runs, and returns what I expect from:
let a1 = 1;
let a2 = () => 2;
let a3 = (foo: any) => foo;
let a4 = null;
let a5 = "Hello!";
let a6 = () => a1;
console.log(ValueTFn.get(a1)); // 1
console.log(ValueTFn.get(a2)); // 2
console.log(ValueTFn.get(a3)); // undefined
console.log(ValueTFn.get(a4)); // null
console.log(ValueTFn.get(a5)); // "Hello!"
a1 = 42;
console.log(ValueTFn.get(a6)); // 42
EDIT:
Actually further testing exposed a different problem with the above:
If I declare something as foo: ValueTFn<boolean>
it's type resolves to
boolean | () => false | () => true
instead of the expected
boolean | () => boolean
...which is a problem, because it means this doesn't compile:
let b: boolean;
func = () => { return b; };
foo: ValueTFn<boolean> = func;
// Error "Type '() => boolean' is not assignable to 'boolean | () => false | () => true'
// Type '() => boolean' is not assignable to '() => false'
// Type 'boolean' is not assignable to 'false'
Is that a different bug?
Why does the compiler over-eagerly resolve <T = boolean> () => T
as () => false | () => true
?? It does so even if strict is off. Tested it on every Playground version from 3.3.3 to Nightly.
I don't think this is a bug. You use
typeof arg
to identify whether the arg is a function, but from the perspective of the TS compiler,arg
may not be a function as you declaredT | (() => T)
. The TS compiler doesn't know what your code does, therefore it is not safe to callarg()
It knows there are only two types, type T
or type () => T
. T
can be anything including a function, but the other type IS definitely a function. If you check the type with a typeof arg === 'function'
then arg
must be callable. It's definitely a bug given the arg
is callable because it is a function. It could be considered that the error message is incorrect and that the message should be something like Type T could be a function that does not match the signature () => T
, but that is against the intent of the developer at that point, because we don't have runtime checking of specific arguments etc. How would be go about describing that T
can be anything, including a function, but that if it matches a specific call signature and return value, then it should execute some other code?
I think the intent of the developer here is to have T be something other than a function, but if it IS a function it should return T. If I pass () => something that is not T
to the function as arg, the TS compiler should error at that point.
function doMyThing <T>(arg: T | () => T): T {
if (typeof arg === "function") { // This should be enough to indicate that arg() is callable
// This should not error
return arg();
}
return arg;
}
// This should error
doMyThing<boolean>(() => "hello");
Sorry for commenting on an old thread, but TS 5.5, there is inferred type guards, making the following code valid:
const isFunction = (x: unknown) => typeof x === "function";
export const correct = <T,>(initialValue: T | (() => T)) => {
return isFunction(initialValue) ? initialValue() : initialValue;
};
where initialValue is correctly inferred as () => T
if it is a function, and T
otherwise. IMO, this is a little better than writing it out the output as T is Function
since it would sync with the function implementation.
It's true that T
could also be a function, but i also agree that typeof "function"
should already narrow it down.
There's 3 solutions in the Playground Link, the second one is weird but it works.
Hope it helps!
type Action<T> = T | ((p: T) => T);
let state: any;
// 1
const setState = <T,>(value: Action<T>) => {
state = value instanceof Function ? value(state) : value;
};
// 2
type NotAFunction = string ...
TypeScript Version: 3.9.0 (Nightly)
Search Terms:
T | (() => T)
,T & Function
Code
Line 2 provides a workaround for this. More info on stackoverflow.
Expected behavior: no errors
Actual behavior:
This expression is not callable. Not all constituents of type '(() => T) | (T & Function)' are callable. Type 'T & Function' has no call signatures.
Playground Link: here.
Related Issues: none