-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8432,6 +8432,10 @@ namespace ts { | |
return createTypeFromGenericGlobalType(globalArrayType, [elementType]); | ||
} | ||
|
||
function createReadonlyArrayType(elementType: Type): ObjectType { | ||
return createTypeFromGenericGlobalType(globalReadonlyArrayType, [elementType]); | ||
} | ||
|
||
function getTypeFromArrayTypeNode(node: ArrayTypeNode): Type { | ||
const links = getNodeLinks(node); | ||
if (!links.resolvedType) { | ||
|
@@ -10087,11 +10091,16 @@ namespace ts { | |
} | ||
|
||
function instantiateMappedType(type: MappedType, mapper: TypeMapper): Type { | ||
// Check if we have a homomorphic mapped type, i.e. a type of the form { [P in keyof T]: X } for some | ||
// type variable T. If so, the mapped type is distributive over a union type and when T is instantiated | ||
// 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 | ||
// operation depends on T as follows: | ||
// * If T is a primitive type no mapping is performed and the result is simply T. | ||
// * If T is a union type we distribute the mapped type over the union. | ||
// * If T is an array we map to an array where the element type has been transformed. | ||
// * If T is a tuple we map to a tuple where the element types have been transformed. | ||
// * Otherwise we map to an object type where the type of each property has been transformed. | ||
// For example, when T is instantiated to a union type A | B, we produce { [P in keyof A]: X } | | ||
// { [P in keyof B]: X }, and when when T is instantiated to a union type A | undefined, we produce | ||
// { [P in keyof A]: X } | undefined. | ||
const constraintType = getConstraintTypeFromMappedType(type); | ||
if (constraintType.flags & TypeFlags.Index) { | ||
const typeVariable = (<IndexType>constraintType).type; | ||
|
@@ -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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
isReadonlyArrayType(t) ? createReadonlyArrayType(instantiateMappedTypeTemplate(type, numberType, /*isOptional*/ true, replacementMapper)) : | ||
isTupleType(t) ? instantiateMappedTupleType(t, type, replacementMapper) : | ||
instantiateAnonymousType(type, replacementMapper); | ||
} | ||
return t; | ||
}); | ||
|
@@ -10114,6 +10127,26 @@ namespace ts { | |
return type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection); | ||
} | ||
|
||
function instantiateMappedTupleType(tupleType: TupleTypeReference, mappedType: MappedType, mapper: TypeMapper) { | ||
const minLength = tupleType.target.minLength; | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth extracting this calculation into a function. |
||
modifiers & MappedTypeModifiers.ExcludeOptional ? getTypeReferenceArity(tupleType) - (tupleType.target.hasRestElement ? 1 : 0) : | ||
minLength; | ||
return createTupleType(elementTypes, newMinLength, tupleType.target.hasRestElement, tupleType.target.associatedNames); | ||
} | ||
|
||
function instantiateMappedTypeTemplate(type: MappedType, key: Type, isOptional: boolean, mapper: TypeMapper) { | ||
const templateMapper = combineTypeMappers(mapper, createTypeMapper([getTypeParameterFromMappedType(type)], [key])); | ||
const propType = instantiateType(getTemplateTypeFromMappedType(<MappedType>type.target || type), templateMapper); | ||
const modifiers = getMappedTypeModifiers(type); | ||
return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !isTypeAssignableTo(undefinedType, propType) ? getOptionalType(propType) : | ||
strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : | ||
propType; | ||
} | ||
|
||
function instantiateAnonymousType(type: AnonymousType, mapper: TypeMapper): AnonymousType { | ||
const result = <AnonymousType>createObjectType(type.objectFlags | ObjectFlags.Instantiated, type.symbol); | ||
if (type.objectFlags & ObjectFlags.Mapped) { | ||
|
@@ -12441,6 +12474,10 @@ namespace ts { | |
return !!(getObjectFlags(type) & ObjectFlags.Reference) && (<TypeReference>type).target === globalArrayType; | ||
} | ||
|
||
function isReadonlyArrayType(type: Type): boolean { | ||
return !!(getObjectFlags(type) & ObjectFlags.Reference) && (<TypeReference>type).target === globalReadonlyArrayType; | ||
} | ||
|
||
function isArrayLikeType(type: Type): boolean { | ||
// A type is array-like if it is a reference to the global Array or global ReadonlyArray type, | ||
// or if it is not the undefined or null type and if it is assignable to ReadonlyArray<any> | ||
|
@@ -12996,6 +13033,22 @@ namespace ts { | |
return undefined; | ||
} | ||
} | ||
// For arrays and tuples we infer new arrays and tuples where the reverse mapping has been | ||
// applied to the element type(s). | ||
if (isArrayType(source)) { | ||
return createArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target)); | ||
} | ||
if (isReadonlyArrayType(source)) { | ||
return createReadonlyArrayType(inferReverseMappedType((<TypeReference>source).typeArguments![0], target)); | ||
} | ||
if (isTupleType(source)) { | ||
const elementTypes = map(source.typeArguments || emptyArray, t => inferReverseMappedType(t, target)); | ||
const minLength = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? | ||
getTypeReferenceArity(source) - (source.target.hasRestElement ? 1 : 0) : source.target.minLength; | ||
return createTupleType(elementTypes, minLength, source.target.hasRestElement, source.target.associatedNames); | ||
} | ||
// For all other object types we infer a new object type where the reverse mapping has been | ||
// applied to the type of each property. | ||
const reversed = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType; | ||
reversed.source = source; | ||
reversed.mappedType = target; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
//// [mappedTypesArraysTuples.ts] | ||
type Box<T> = { value: T }; | ||
type Boxified<T> = { [P in keyof T]: Box<T[P]> }; | ||
|
||
type T00 = Boxified<[number, string?, ...boolean[]]>; | ||
type T01 = Partial<[number, string?, ...boolean[]]>; | ||
type T02 = Required<[number, string?, ...boolean[]]>; | ||
|
||
type T10 = Boxified<string[]>; | ||
type T11 = Partial<string[]>; | ||
type T12 = Required<string[]>; | ||
type T13 = Boxified<ReadonlyArray<string>>; | ||
type T14 = Partial<ReadonlyArray<string>>; | ||
type T15 = Required<ReadonlyArray<string>>; | ||
|
||
type T20 = Boxified<(string | undefined)[]>; | ||
type T21 = Partial<(string | undefined)[]>; | ||
type T22 = Required<(string | undefined)[]>; | ||
type T23 = Boxified<ReadonlyArray<string | undefined>>; | ||
type T24 = Partial<ReadonlyArray<string | undefined>>; | ||
type T25 = Required<ReadonlyArray<string | undefined>>; | ||
|
||
type T30 = Boxified<Partial<string[]>>; | ||
type T31 = Partial<Boxified<string[]>>; | ||
|
||
type A = { a: string }; | ||
type B = { b: string }; | ||
|
||
type T40 = Boxified<A | A[] | ReadonlyArray<A> | [A, B] | string | string[]>; | ||
|
||
declare function unboxify<T>(x: Boxified<T>): T; | ||
|
||
declare let x10: [Box<number>, Box<string>, ...Box<boolean>[]]; | ||
let y10 = unboxify(x10); | ||
|
||
declare let x11: Box<number>[]; | ||
let y11 = unboxify(x11); | ||
|
||
declare let x12: { a: Box<number>, b: Box<string[]> }; | ||
let y12 = unboxify(x12); | ||
|
||
declare function nonpartial<T>(x: Partial<T>): T; | ||
|
||
declare let x20: [number | undefined, string?, ...boolean[]]; | ||
let y20 = nonpartial(x20); | ||
|
||
declare let x21: (number | undefined)[]; | ||
let y21 = nonpartial(x21); | ||
|
||
declare let x22: { a: number | undefined, b?: string[] }; | ||
let y22 = nonpartial(x22); | ||
|
||
type Awaited<T> = T extends PromiseLike<infer U> ? U : T; | ||
type Awaitified<T> = { [P in keyof T]: Awaited<T[P]> }; | ||
|
||
declare function all<T extends any[]>(...values: T): Promise<Awaitified<T>>; | ||
|
||
function f1(a: number, b: Promise<number>, c: string[], d: Promise<string[]>) { | ||
let x1 = all(a); | ||
let x2 = all(a, b); | ||
let x3 = all(a, b, c); | ||
let x4 = all(a, b, c, d); | ||
} | ||
|
||
|
||
//// [mappedTypesArraysTuples.js] | ||
"use strict"; | ||
var y10 = unboxify(x10); | ||
var y11 = unboxify(x11); | ||
var y12 = unboxify(x12); | ||
var y20 = nonpartial(x20); | ||
var y21 = nonpartial(x21); | ||
var y22 = nonpartial(x22); | ||
function f1(a, b, c, d) { | ||
var x1 = all(a); | ||
var x2 = all(a, b); | ||
var x3 = all(a, b, c); | ||
var x4 = all(a, b, c, d); | ||
} | ||
|
||
|
||
//// [mappedTypesArraysTuples.d.ts] | ||
declare type Box<T> = { | ||
value: T; | ||
}; | ||
declare type Boxified<T> = { | ||
[P in keyof T]: Box<T[P]>; | ||
}; | ||
declare type T00 = Boxified<[number, string?, ...boolean[]]>; | ||
declare type T01 = Partial<[number, string?, ...boolean[]]>; | ||
declare type T02 = Required<[number, string?, ...boolean[]]>; | ||
declare type T10 = Boxified<string[]>; | ||
declare type T11 = Partial<string[]>; | ||
declare type T12 = Required<string[]>; | ||
declare type T13 = Boxified<ReadonlyArray<string>>; | ||
declare type T14 = Partial<ReadonlyArray<string>>; | ||
declare type T15 = Required<ReadonlyArray<string>>; | ||
declare type T20 = Boxified<(string | undefined)[]>; | ||
declare type T21 = Partial<(string | undefined)[]>; | ||
declare type T22 = Required<(string | undefined)[]>; | ||
declare type T23 = Boxified<ReadonlyArray<string | undefined>>; | ||
declare type T24 = Partial<ReadonlyArray<string | undefined>>; | ||
declare type T25 = Required<ReadonlyArray<string | undefined>>; | ||
declare type T30 = Boxified<Partial<string[]>>; | ||
declare type T31 = Partial<Boxified<string[]>>; | ||
declare type A = { | ||
a: string; | ||
}; | ||
declare type B = { | ||
b: string; | ||
}; | ||
declare type T40 = Boxified<A | A[] | ReadonlyArray<A> | [A, B] | string | string[]>; | ||
declare function unboxify<T>(x: Boxified<T>): T; | ||
declare let x10: [Box<number>, Box<string>, ...Box<boolean>[]]; | ||
declare let y10: [number, string, ...boolean[]]; | ||
declare let x11: Box<number>[]; | ||
declare let y11: number[]; | ||
declare let x12: { | ||
a: Box<number>; | ||
b: Box<string[]>; | ||
}; | ||
declare let y12: { | ||
a: number; | ||
b: string[]; | ||
}; | ||
declare function nonpartial<T>(x: Partial<T>): T; | ||
declare let x20: [number | undefined, string?, ...boolean[]]; | ||
declare let y20: [number, string, ...boolean[]]; | ||
declare let x21: (number | undefined)[]; | ||
declare let y21: number[]; | ||
declare let x22: { | ||
a: number | undefined; | ||
b?: string[]; | ||
}; | ||
declare let y22: { | ||
a: number; | ||
b: string[]; | ||
}; | ||
declare type Awaited<T> = T extends PromiseLike<infer U> ? U : T; | ||
declare type Awaitified<T> = { | ||
[P in keyof T]: Awaited<T[P]>; | ||
}; | ||
declare function all<T extends any[]>(...values: T): Promise<Awaitified<T>>; | ||
declare function f1(a: number, b: Promise<number>, c: string[], d: Promise<string[]>): void; |
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