TypeScript Version: 3.9.0 (Nightly)
Search Terms: T | (() => T), T & Function
Code
type Initializer<T> = T | (() => T)
// type Initializer<T> = T extends any ? (T | (() => T)) : never
function correct<T>(arg: Initializer<T>) {
return typeof arg === 'function' ? arg() : arg // error
}
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
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?
Most helpful comment
Simplified.