-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
T | (() => T) #37663
Comments
This check isn't safe if |
@DanielRosenwasser Why does the following alternative work then?
In addition, I expected
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 Appreciate your thoughts, thanks! |
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
However, this isn't directly related to the main issue. The question remains, if both |
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)
} |
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. // 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. |
@falsandtru well summarized the issue. I would add one case:
Plagyround with all three examples @dhmk083 Your example seems to describe a different issue related to (unresolved) conditional types. Not sure why |
@bela53 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 type SetStateAction<S> = S | ((prevState: S) => S); If using |
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 NOTE: this workaround can have problems in some situations: #37663 (comment) Check a safer (though more verbose) workaround: #37663 (comment)) |
FYI:
|
Be careful with using |
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 You know, I could have sworn this method of using 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: Possible reason...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 const isFunction = (x: unknown): x is Function => typeof x === 'function' Here's an example of it in action with a nice |
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 #37663 (comment), 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 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
} |
This seems to be a TypeScript issue, see microsoft/TypeScript#37663 Fixed: Elephantina#1
I don't think this is a bug. You use |
I think a safer approach would be to let the developer decide what is or isn't type 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 |
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: 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? |
It knows there are only two types, type 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 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 |
It's true that 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
The text was updated successfully, but these errors were encountered: