Skip to content
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

Can't dereference array type in conditional type #22985

Closed
ceymard opened this issue Mar 29, 2018 · 7 comments
Closed

Can't dereference array type in conditional type #22985

ceymard opened this issue Mar 29, 2018 · 7 comments
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@ceymard
Copy link

ceymard commented Mar 29, 2018

TypeScript Version: 2.8.1 and 2.9.0-dev.20180329

Search Terms:

I tried actually a lot of terms around mapped types, arrays, conditional types, array member dereferencing in mapped types etc..

Code

export type RecursivePartial<T> = {
  [P in keyof T]?: T[P] extends Array<any> ? {[index: number]: RecursivePartial<T[P][0]>} :
    T[P] extends object ? RecursivePartial<T[P]> : T[P];
};

var a = {o: 1, b: 2, c: [{a: 1, c: '213'}]}
function assign<T>(o: T, a: RecursivePartial<T>) { }
assign(a, {o: 2, c: {0: {a: 2, c: '213123'}}})

Expected behavior:

T[P][0] is usable and doesn't produce an error, since T[P] extends any[] and should thus be indexable on [0] (it seems this is the way to dereference an array)

Actual behavior:
I am getting type 0 cannot be used to index type T[P].

The odd part is that behaviour-wise it seems to be working ! I can't change the type of the nested c to anything other than string and I can't create random properties or what.

Am I missing something ?

@RyanCavanaugh
Copy link
Member

Not sure why that's not allowed. @ahejlsberg / @weswigham ?

FWIW I would write it this way:

export type RecursivePartial<T> = {
  [P in keyof T]?: T[P] extends Array<infer E> ? {[index: number]: RecursivePartial<E>} :
    T[P] extends object ? RecursivePartial<T[P]> : T[P];
};

@mhegazy
Copy link
Contributor

mhegazy commented Mar 29, 2018

The compiler does not recognize the constraints on anything that is not a naked type parameter in a conditional type in the true branch. so T[P] extends any[] does not narrow T[P] to array in the true branch. there are two ways to accomplish that, either use infer as @RyanCavanaugh noted. the other is to just move it to a diffrent type alias declaration:

export type RecursivePartial<T> = {
  [P in keyof T]?: Recursive<T[P]>;
};

type Recursive<T> = T extends Array<any> ? NI<T[number]>:
    T extends object ? RecursivePartial<T> : T;

type NI<T> = { [x: number]: T };

@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Mar 29, 2018
@mhegazy
Copy link
Contributor

mhegazy commented Mar 29, 2018

We probably should rethink our design choices here. it is confusing that T[P] extends U does not work like T extends U.

@weswigham
Copy link
Member

weswigham commented Mar 29, 2018

The compiler does not recognize the constraints on anything that is not a naked type parameter in a conditional type in the true branch.

Actually I think @ahejlsberg just recently changed it (#22707) so we manufacture constraints for indexes and matching tuples, too. We should have it constrained here, afaik.

@ahejlsberg
Copy link
Member

It's a bug. The issue here is that we use isPartOfTypeNode in the parent node walk that attaches synthetic constraints to type variables, but isPartOfTypeNode returns false for property and parameter declarations so we end up stopping prematurely. I will fix to use some other stopping condition.

@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Design Limitation Constraints of the existing architecture prevent this from being fixed labels Mar 29, 2018
@ahejlsberg ahejlsberg self-assigned this Mar 29, 2018
@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Mar 29, 2018
@ahejlsberg
Copy link
Member

ahejlsberg commented Mar 29, 2018

@ceymard With the fix your example now compiles. That said, I'd suggest refactoring the conditional type inside the mapped type into a separate type alias:

export type RecursivePartial<T> = {
    [P in keyof T]?: RecursivePartialItem<T[P]>;
};

type RecursivePartialItem<T> =
    T extends Array<any> ? { [index: number]: RecursivePartial<T[0]> } :
    T extends object ? RecursivePartial<T> :
    T;

This type is distributive (because it operates on a naked type parameter) which ensures that the operation spreads itself over union types (e.g. RecursivePartialItem<Foo | undefined> becomes RecursivePartalItem<Foo> | RecursivePartialItem<undefined>). Without this change, your example will do nothing to properties with union types (such as optional properties). Read more about distributive conditional types here #21316 (comment).

@sirian
Copy link
Contributor

sirian commented Mar 29, 2018

@ceymard try infer T[P] like this


export type RecursivePartial<T> = {
    [P in keyof T]?: T[P] extends (infer A)[] ? {[index: number]: RecursivePartial<A>} :
        T[P] extends object ? RecursivePartial<T[P]> : T[P];
};

const a = {o: 1, b: 2, c: [{a: 1, b: ''}]};
function assign<T>(o: T, a: RecursivePartial<T>) { }
assign(a, {o: 2, c: {0: {a: 2, c: '213123'}}});

image

@mhegazy mhegazy modified the milestones: TypeScript 2.9, TypeScript 2.8.2 Mar 30, 2018
@microsoft microsoft locked and limited conversation to collaborators Jul 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

6 participants