-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Improved mapped type support for arrays and tuples #26063
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One small question/comment.
// to a union type A | B, we produce { [P in keyof A]: X } | { [P in keyof B]: X }. Furthermore, for | ||
// homomorphic mapped types we leave primitive types alone. For example, when T is instantiated to a | ||
// union type A | undefined, we produce { [P in keyof A]: X } | undefined. | ||
// For a momomorphic mapped type { [P in keyof T]: X }, where T is some type variable, the mapping |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo:homomorphic
const elementTypes = map(tupleType.typeArguments || emptyArray, (_, i) => | ||
instantiateMappedTypeTemplate(mappedType, getLiteralType("" + i), i >= minLength, mapper)); | ||
const modifiers = getMappedTypeModifiers(mappedType); | ||
const newMinLength = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth extracting this calculation into a function.
@@ -10100,7 +10109,11 @@ namespace ts { | |||
if (typeVariable !== mappedTypeVariable) { | |||
return mapType(mappedTypeVariable, t => { | |||
if (isMappableType(t)) { | |||
return instantiateAnonymousType(type, createReplacementMapper(typeVariable, t, mapper)); | |||
const replacementMapper = createReplacementMapper(typeVariable, t, mapper); | |||
return isArrayType(t) ? createArrayType(instantiateMappedTypeTemplate(type, numberType, /*isOptional*/ true, replacementMapper)) : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isOptional doesn’t have any meaning for arrays, does it? Why not pass false if that’s true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The isOptional
indicates whether a -?
modifier on the mapped type should strip undefined
from the source type. We want to do that when the source type originates in an optional property, but we also want to do it when the source type is an array element type.
First: this is awesome. Next: I know there were questions around still being able to map arrays/tuples like objects if the need arises. Below are some constructs that seem to allow that ( type PromisifyNormal<T> = { [K in keyof T]: Promise<T[K]> }
type PromisifyNormalObject = PromisifyNormal<{ a: string, b: number, c: boolean }>
// {a: Promise<string>, b: Promise<number>, c: Promise<boolean>}
type PromisifyNormalTuple = PromisifyNormal<[string, number, boolean]>;
// [Promise<string>, Promise<number>, Promise<boolean>]
type PromisifySomeKeys<T, KT extends keyof T = keyof T> = { [K in KT]: Promise<T[K]> }
type PromisifySomeKeysObject = PromisifySomeKeys<{ a: string, b: number, c: boolean }>
// {a: Promise<string>, b: Promise<number>, c: Promise<boolean>}
type PromisifySomeKeysTuple = PromisifySomeKeys<[string, number, boolean]>;
/*
type PromisifySomeKeysTuple = {
[x: number]: Promise<string | number | boolean>;
"0": Promise<string>;
"1": Promise<number>;
"2": Promise<boolean>;
length: Promise<3>;
...
}
*/
type PromisifyIntersectionTuple = PromisifyNormal<[string, number, boolean] & { randomProp: 1234 }>;
/*
type PromisifyIntersectionTuple = {
[x: number]: Promise<string | number | boolean>;
"0": Promise<string>;
"1": Promise<number>;
"2": Promise<boolean>;
length: Promise<3>;
...
randomProp: Promise<1234>;
}
*/ |
This is really cool and I actually just asked if this was possible in 3.0 here. I have had a play with this with the insiders build and it does exactly what I want:
my only comment would be that the type of
whereas I would expect |
As far as I can tell this PR does not help in the case of the rxjs pipe function:
here the second type param of one element in the tuple is the first type param of the next element in the tuple. Is this case currently supported? |
@ahejlsberg I've been playing a bit with this and the compiler does seem not recognize the fact that a mapped array type will still be an array type for the purpose of rest parameters. Ex on
Is this a bug, or design limitation. Will it be addressed in the future ? |
@dragomirtitian I think that's being tracked in #26163. |
@jcalz I don't think that issue fixes the specific case of rest types because they use a different assignment relation. I think the problem is that the rest type checking uses |
@dragomirtitian @jcalz @jack-williams The declare let ps: Promise<string>;
declare let px: Promise<number[]>;
const r = invokeWhenReady((s, x) => ({ s, x }), ps, px) // Promise<{ s: string, x: number[] }> |
For anyone who is struggling with prototype properties getting mapped as well (like in @Roaders's #26063 (comment)), causing type Boxified<T> = {
[P in keyof T]: P extends keyof [] ? T[P] : Box<T[P]>
}; instead of type Boxified<T> = {
[P in keyof T]: Box<T[P]>
}; (It worked implicitly in |
@ahejlsberg it appears this only works on type aliases, rather than any homomorphic mapped type?
It's always been surprising to me when the left and right hand sides of a type alias aren't interchangeable. At this point I wish an operator like |
The higher-order behavior only applies to generics, so bare object type expressions are never homomorphic; only aliases are. |
Sometimes I wish I could do a one-off tuple mapping inline instead of having to declare a type alias out of line. And very often, I wish I could distribute over a union type inline instead of using a distributive conditional type alias |
See #27995 |
This PR improves our support for arrays and tuples in homomorphic mapped types (i.e. structure preserving mapped types of the form
{ [P in keyof T]: X }
). When a homomorphic mapped type is applied to an array or tuple type, we now produce a corresponding array or tuple type where the element type(s) have been transformed.Previously, we would treat array and tuple types like regular object types and transform all properties (including methods) of the arrays and tuples. This behavior is rarely if ever desired.
Given a homomorphic mapped type
{ [P in keyof T]: X }
, whereT
is some type variable, the mapping operation depends onT
as follows (the first two rules are existing behavior and the remaining are introduced by this PR):S[]
we map to an array typeR[]
, whereR
is an instantiation ofX
withS
substituted forT[P]
.ReadonlyArray<S>
we map to an array typeReadonlyArray<R>
, whereR
is an instantiation ofX
withS
substituted forT[P]
.[S0, S1, ..., Sn]
we map to a tuple type[R0, R1, ..., Rn]
, where eachRx
is an instantiation ofX
with the correspondingSx
substituted forT[P]
.Homomorphic mapped types can use
?
,-?
, or+?
annotations to modify the optional-ness of tuple element types. For example, the predefinedPartial<T>
andRequired<T>
types have the expected effects on tuple element types:In
--strictNullChecks
mode the?
,-?
, or+?
annotations also add or removeundefined
from the element type(s) of arrays and tuples:A
readonly
,-readonly
, or+readonly
annotation in a homomorphic mapped type currently has no effect on array or tuple elements (we might consider mapping fromArray
toReadonlyArray
and vice versa, although that technically isn't structure preserving because it adds or removes methods).Homomorphic mapped type support for tuples makes it possible to transform variable length parameter lists, eliminating the need for repetitive patterns overloads in several scenarios. For example:
This PR implements much of what is suggested in #25947, but without introducing new syntax.