Skip to content
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

Merged
merged 4 commits into from Jul 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 59 additions & 6 deletions src/compiler/checker.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo:homomorphic

// 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;
Expand All @@ -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)) :
Copy link
Member

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?

Copy link
Member Author

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.

isReadonlyArrayType(t) ? createReadonlyArrayType(instantiateMappedTypeTemplate(type, numberType, /*isOptional*/ true, replacementMapper)) :
isTupleType(t) ? instantiateMappedTupleType(t, type, replacementMapper) :
instantiateAnonymousType(type, replacementMapper);
}
return t;
});
Expand All @@ -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 :
Copy link
Member

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.

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) {
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/mappedTypes4.types
Expand Up @@ -103,7 +103,7 @@ type T01 = Readonly<A | B | C | null | undefined>;
>null : null

type T02 = Boxified<A | B[] | C | string>
>T02 : string | Boxified<A> | Boxified<C> | Boxified<B[]>
>T02 : string | Boxified<A> | Boxified<C> | Box<B>[]
>Boxified : Boxified<T>
>A : A
>B : B
Expand Down
144 changes: 144 additions & 0 deletions tests/baselines/reference/mappedTypesArraysTuples.js
@@ -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;