Skip to content

Destructuring array result of function with generics call sometimes lead to type loss #32003

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

Closed
mrblackus opened this issue Jun 20, 2019 · 6 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@mrblackus
Copy link

mrblackus commented Jun 20, 2019

Hi!

TypeScript Version: 3.5.2

Search Terms: destructuring result function generics type loss any

Problem is only with snippet 2, when I get the third parameter with destructuring, the first one became any instead of the generic type. The problem doesn't show if the generic is explicitly provided. Snippets 1, 3 and 4 works as expected.

Code

function foo<T = {}>(): [T, string, number] {
	let a: T;
	return [a, 'foo', 2];
}

// 1
const [a, b] = foo();
// a: {}, b: string

// 2
const [x, y, z] = foo();
// x: any, y: string, z: number

// 3
const res = foo();
const [x2, y2, z2] = res;
// x2: {}, y: string, z: number

// 4
const [x3, y3, z3] = foo<{ bar: string }>();
// x3: { bar: string }, y3: string, z: number

Expected behavior:
x type is {}

Actual behavior:
x type is any

Playground Link: https://www.typescriptlang.org/play/#src=%0D%0Afunction%20foo%3CT%20%3D%20%7B%7D%3E()%3A%20%5BT%2C%20string%2C%20number%5D%20%7B%0D%0A%09let%20a%3A%20T%3B%0D%0A%09return%20%5Ba%2C%20'foo'%2C%202%5D%3B%0D%0A%7D%0D%0A%0D%0Aconst%20%5Ba%2C%20b%5D%20%3D%20foo()%3B%0D%0A%2F%2F%20a%3A%20%7B%7D%2C%20b%3A%20string%0D%0A%0D%0Aconst%20%5Bx%2C%20y%2C%20z%5D%20%3D%20foo()%3B%0D%0A%2F%2F%20x%3A%20any%2C%20y%3A%20string%2C%20z%3A%20number%0D%0A%0D%0Aconst%20res%20%3D%20foo()%3B%0D%0Aconst%20%5Bx2%2C%20y2%2C%20z2%5D%20%3D%20res%3B%0D%0A%2F%2F%20x2%3A%20%7B%7D%2C%20y%3A%20string%2C%20z%3A%20number%0D%0A%0D%0Aconst%20%5Bx3%2C%20y3%2C%20z3%5D%20%3D%20foo%3C%7B%20bar%3A%20string%20%7D%3E()%3B%0D%0A%2F%2F%20x3%3A%20%7B%20bar%3A%20string%20%7D%2C%20y3%3A%20string%2C%20z%3A%20number

Related Issues:
Some issues are covering topics that seems related, but I couldn't find one with my specific issue, sorry if there's already a duplicate for this one!

Thanks :)

@ahejlsberg
Copy link
Member

This one is a bit strange. In examples 1 and 2, the call to foo() is contextually typed by the implied types of the array destrucuring patterns ([any, any] and [any, any, any] respectively). Since foo is generic and we have a contextual type, we perform generic return type inference (#16072). In example 2 where the arities of the tuple types match we then infer any for T, and since there is nothing else to make inferences from, that's what we end up with. In the other examples, we make no inferences for T at all and therefore end up with the default {}.

Ideally we shouldn't make any inferences from the implied types of the destructuring patterns, but in other scenarios we do want a contextual type of a tuple type to indicate that array literals should be treated as tuples. So we have conflicting goals and there isn't an obvious simple fix. Add to that the anti-pattern of having a type parameter that isn't used in anywhere in the parameter list (it's basically just a disguised type assertion) and I think we'll call this a design limitation.

@ahejlsberg ahejlsberg added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jun 21, 2019
@miloszpp
Copy link
Contributor

Just wanted to point out that this issue impacts typing hooks in React.

const [pill, setPill] = useState();

The type of pill is inferred to any while it should be undefined according to the type definition:

function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

@danieloprado
Copy link

A little workaround:

function foo<T = {}>(): [T, string, number, undefined] { // <~ add a 4th return
	let a: T;
	return [a, 'foo', 2, undefined]; // <~ return the 4th value as undefined
}

// 1
const [a, b] = foo();
// a: {}, b: string

// 2
const [x, y, z] = foo();
// x:{}, y: string, z: number 👍

// 3
const res = foo();
const [x2, y2, z2] = res;
// x2: {}, y: string, z: number

// 4
const [x3, y3, z3] = foo<{ bar: string }>();
// x3: { bar: string }, y3: string, z: number

@TLadd
Copy link

TLadd commented Feb 6, 2020

Ran into this as well:

export interface Sizes {
  width: number | null;
  height: number | null;
}

  declare function useResizeAware<S = Sizes>(
    customReporter?: (target: HTMLElement | null) => S
  ): [string, S];


const [a, b] = useResizeAware();

const c = useResizeAware();
const d = c[1]

So in my case the generic does appear in the arguments but it is an optional parameter. d is properly typed as Sizes while b is of type any.

Using overrides is another solution that fixed this for me:

  declare function useResizeAware(): [string, Sizes];
  declare function useResizeAware<S>(
    customReporter: (target: HTMLElement | null) => S
  ): [string, S];

@luokuning
Copy link

Looks like TypeScript v3.8 have fixed that issue?

@mrblackus
Copy link
Author

In can confirm that issue is fixed in both TS 3.8 and 3.9. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

6 participants