Closed matt-hensley closed 6 years ago
Wow, that's pretty much the same; isn't it: global augmentation.
@jcalz: this thread had been linked there before, so that'd make sense yeah. :)
You can currently do some if
s 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.
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?
}
@jcalz ah, good catch! Yeah, can't think of a way to support unions this way :/
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
.
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?
@inad9300 I too stumbled upon this thread with the intent of making a DeepPartial type.
@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
}
}
@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!
@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.
@jcalz @inad9300 @vultix @tao-cumplido: I added a DeepPartial
based on the new conditional types in #21316.
12114 added mapped types, including recursive mapped types. But as pointed out by @ahejlsberg
Conditional mapping would greatly improve the ergonomics of libraries like Immutable.js.