microsoft / TypeScript

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

Mapped conditional types #12424

Closed matt-hensley closed 6 years ago

matt-hensley commented 7 years ago

12114 added mapped types, including recursive mapped types. But as pointed out by @ahejlsberg

Note, however, that such types aren't particularly useful without some form of conditional type that makes it possible to limit the recursion to selected kinds of types.

type primitive = string | number | boolean | undefined | null;
type DeepReadonly<T> = T extends primitive ? T : DeepReadonlyObject<T>;
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};

I couldn't find an existing issue with a feature request.

Conditional mapping would greatly improve the ergonomics of libraries like Immutable.js.

jcalz commented 7 years ago

Wow, that's pretty much the same; isn't it: global augmentation.

KiaraGrouwstra commented 7 years ago

@jcalz: this thread had been linked there before, so that'd make sense yeah. :)

niieani commented 6 years ago

You can currently do some ifs to assert certain types (well, not types exactly but type shapes). See my example of a recursive readonly that follows inside Arrays, but doesn't affect booleans/strings/numbers: Playground. Thanks to @tycho01 for some of the helpers.

jcalz commented 6 years ago

Does it map over unions? That's one of the stumbling blocks that I didn't think we could overcome without some changes to the compiler:

declare var test: RecursiveReadonly<{ foo: number | number[] }>
if (typeof test.foo != 'number') {
  test.foo[0] = 1; // no error?
}
niieani commented 6 years ago

@jcalz ah, good catch! Yeah, can't think of a way to support unions this way :/

KiaraGrouwstra commented 6 years ago

Yeah, so IsArrayType is not union-proof in T. It relies on this DefinitelyYes, which here intends to aggregate the check results of different ArrayPrototypeProperties keys, but logically the results should remain separated for different T union elements.

We do not yet have anything like union iteration to address that today. We'd want an IsUnion too in that case, but the best I came up with could only distinguish string literals vs. unions thereof.

I'd really need to document which types are union-proof in which parameters, as this won't be the only type that'd break on this.

I'm actually thinking in this case the globals augmentation method to identify prototypes might do better in terms of staying union-proof than my IsArrayType.

inad9300 commented 6 years ago

I'm trying to implement a DeepPartial<T> interface which recursively makes optional all the properties of the given type. I've noticed that function signatures are not checked by TypeScript after applying it, i.e.

type DeepPartial<T> = {
    [P in keyof T]?: DeepPartial<T[P]>
}

interface I {
    fn: (a: string) => void
    n: number
}

let v: DeepPartial<I> = {
    fn: (a: number) => {}, // The compiler is happy -- bad.
    n: '' // The compiler complains -- good.
}

Is this something that the current proposal could solve as well?

vultix commented 6 years ago

@inad9300 I too stumbled upon this thread with the intent of making a DeepPartial type.

tao-cumplido commented 6 years ago

@inad9300 @vultix I was working on something with the concepts discussed here and realized it might work to make a DeepPartial that works with functions too.

type False = '0';
type True = '1';
type If<C extends True | False, Then, Else> = { '0': Else, '1': Then }[C];

type Diff<T extends string, U extends string> = (
    { [P in T]: P } & { [P in U]: never } & { [x: string]: never }
)[T];

type X<T> = Diff<keyof T, keyof Object>

type Is<T, U> = (Record<X<T & U>, False> & Record<any, True>)[Diff<X<T>, X<U>>]

type DeepPartial<T> = {
    [P in keyof T]?: If<Is<Function & T[P], Function>, T[P], DeepPartial<T[P]>>
}

I haven't tested it thoroughly but it worked with the example you provided.

Edit: I just realized that it doesn't work in every case. Specifically if the nested object's keyset is a subset of Function's keyset.

type I = DeepPartial<{
    fn: () => void,
    works: {
        foo: () => any,
    },
    fails: {
        apply: any,
    }
}>

// equivalent to:

type J = {
    fn?: () => void,
    works?: {
        foo?: () => any,
    },
    fails?: {
        apply: any // not optional
    }
}
inad9300 commented 6 years ago

@tao-cumplido What you did there is mind tangling, but admirable. It's a real pity that is not covering all the cases, but it works better than the common approach. Thank you!

tao-cumplido commented 6 years ago

@inad9300 I found a version that works better:

type DeepPartial<T> = {
    [P in keyof T]?: If<Is<T[P], object>, T[P], DeepPartial<T[P]>>
}

It no longer tests against Function which solves the problem above. I still found another case that doesn't work (it didn't in the first version either): when you create a union with a string-indexed type it fails. But the upcoming conditional types should allow a straightforward DeepPartial.

KiaraGrouwstra commented 6 years ago

@jcalz @inad9300 @vultix @tao-cumplido: I added a DeepPartial based on the new conditional types in #21316.