microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.17k stars 12.51k forks source link

T | (() => T) #37663

Open dhmk083 opened 4 years ago

dhmk083 commented 4 years ago

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

DanielRosenwasser commented 4 years ago

This check isn't safe if T is instantiated with something like (x: string) => number.

bela53 commented 4 years ago

@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
}

Playground

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!

zerkms commented 4 years ago

@bela53

const r2 = correct<(_: string) => number>((v: string) => v.toString().length)

this code passes type validation but blows in runtime.

bela53 commented 4 years ago

@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.

DanielRosenwasser commented 4 years ago

that seems like a bug

falsandtru commented 4 years ago

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)
}
dhmk083 commented 4 years ago

@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.

dhmk083 commented 4 years ago

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
}

Playground

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, but works for Record<K, T>. I think they should either all work or all fail.

bela53 commented 4 years ago

@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.

dhmk083 commented 4 years ago

@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.

jack-williams commented 4 years ago

Is this related #27422?

Svish commented 3 years ago

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?

kentcdodds commented 3 years ago

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)

misha-erm commented 3 years ago

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
    }
}

Playground

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 :)

jsejcksn commented 3 years ago

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.

kentcdodds commented 3 years ago

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 :)

jsejcksn commented 3 years ago

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;
}

Playground

smac89 commented 2 years ago

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.

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

rpearce commented 2 years ago

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

Elia-Darwish commented 2 years ago

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'
}
WoodyWoodsta commented 2 years ago

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?

RyanCavanaugh commented 2 years ago

@WoodyWoodsta this bug is fully triaged. What do you mean?

WoodyWoodsta commented 2 years ago

@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.

keyboard3 commented 2 years ago

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
}
mnpenner commented 1 year ago

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
}
pspdragon commented 1 year ago

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()

EltonLobo07 commented 1 year ago

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();
}
ITenthusiasm commented 1 year ago

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).

AnorZaken commented 1 year ago

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

Playground

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.

Irrelon commented 12 months ago

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()

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");
jeremy-code commented 6 months ago

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.

joaorr3 commented 1 week ago

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 ...

Playground Link