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 ?
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'}}});

Most helpful comment
Not sure why that's not allowed. @ahejlsberg / @weswigham ?
FWIW I would write it this way: