Typescript: Can't dereference array type in conditional type

Created on 29 Mar 2018  路  7Comments  路  Source: microsoft/TypeScript


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 ?

Bug Fixed

Most helpful comment

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

All 7 comments

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

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

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

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.

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.

@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 https://github.com/Microsoft/TypeScript/pull/21316#issue-164138025.

@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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antony-Jones picture Antony-Jones  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

wmaurer picture wmaurer  路  3Comments

MartynasZilinskas picture MartynasZilinskas  路  3Comments