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

Apply a contextual type to empty arrays #51882

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18675,7 +18675,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ObjectLiteralExpression:
return some((node as ObjectLiteralExpression).properties, isContextSensitive);
case SyntaxKind.ArrayLiteralExpression:
return some((node as ArrayLiteralExpression).elements, isContextSensitive);
if ((node as ArrayLiteralExpression).elements.length > 0) {
return some((node as ArrayLiteralExpression).elements, isContextSensitive);
}
return true;
case SyntaxKind.ConditionalExpression:
return isContextSensitive((node as ConditionalExpression).whenTrue) ||
isContextSensitive((node as ConditionalExpression).whenFalse);
Expand Down Expand Up @@ -29121,6 +29124,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (forceTuple || inConstContext || contextualType && someType(contextualType, isTupleLikeType)) {
return createArrayLiteralType(createTupleType(elementTypes, elementFlags, /*readonly*/ inConstContext));
}
if (elementCount === 0 && contextualType && !checkMode) {
// For an empty array literal, produce the type (T1 & T2 & T3...)[] where Tn is the element type
// of each array-like union constituent of the contextual type
const contextualElementTypes: Type[] = [];
forEachType(contextualType, type => {
if (isArrayLikeType(type)) {
// Note that if we're here, it's not a tuple contextual type, so the
// numeric index signature must be present and sufficient
const elementType = getIndexTypeOfType(type, numberType);
if (elementType) {
contextualElementTypes.push(elementType);
}
}
});
// It's possible that zero contextual types are arraylike
if (contextualElementTypes.length > 1) {
return createArrayType(getIntersectionType(contextualElementTypes));
}
else if (contextualElementTypes.length === 1) {
return createArrayType(contextualElementTypes[0]);
}
}
return createArrayLiteralType(createArrayType(elementTypes.length ?
getUnionType(sameMap(elementTypes, (t, i) => elementFlags[i] & ElementFlags.Variadic ? getIndexedAccessTypeOrUndefined(t, numberType) || anyType : t), UnionReduction.Subtype) :
strictNullChecks ? implicitNeverType : undefinedWideningType, inConstContext));
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/accessorsOverrideProperty9.types
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ApiItem {
>members : readonly ApiItem[]

return [];
>[] : never[]
>[] : ApiItem[]
}
}

Expand Down Expand Up @@ -63,7 +63,7 @@ function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
>members : readonly ApiItem[]

return [];
>[] : never[]
>[] : ApiItem[]
}
}

Expand All @@ -83,7 +83,7 @@ export class ApiEnum extends ApiItemContainerMixin(ApiItem) {
>members : readonly ApiEnumMember[]

return [];
>[] : never[]
>[] : ApiEnumMember[]
}
}

16 changes: 8 additions & 8 deletions tests/baselines/reference/arrayAssignmentTest1.types
Original file line number Diff line number Diff line change
Expand Up @@ -81,35 +81,35 @@ var f1 = function () { return new C1();}

var arr_any: any[] = [];
>arr_any : any[]
>[] : undefined[]
>[] : any[]

var arr_i1: I1[] = [];
>arr_i1 : I1[]
>[] : undefined[]
>[] : I1[]

var arr_c1: C1[] = [];
>arr_c1 : C1[]
>[] : undefined[]
>[] : C1[]

var arr_c2: C2[] = [];
>arr_c2 : C2[]
>[] : undefined[]
>[] : C2[]

var arr_i1_2: I1[] = [];
>arr_i1_2 : I1[]
>[] : undefined[]
>[] : I1[]

var arr_c1_2: C1[] = [];
>arr_c1_2 : C1[]
>[] : undefined[]
>[] : C1[]

var arr_c2_2: C2[] = [];
>arr_c2_2 : C2[]
>[] : undefined[]
>[] : C2[]

var arr_c3: C3[] = [];
>arr_c3 : C3[]
>[] : undefined[]
>[] : C3[]

var i1_error: I1 = []; // should be an error - is
>i1_error : I1
Expand Down
16 changes: 8 additions & 8 deletions tests/baselines/reference/arrayAssignmentTest2.types
Original file line number Diff line number Diff line change
Expand Up @@ -81,35 +81,35 @@ var f1 = function () { return new C1();}

var arr_any: any[] = [];
>arr_any : any[]
>[] : undefined[]
>[] : any[]

var arr_i1: I1[] = [];
>arr_i1 : I1[]
>[] : undefined[]
>[] : I1[]

var arr_c1: C1[] = [];
>arr_c1 : C1[]
>[] : undefined[]
>[] : C1[]

var arr_c2: C2[] = [];
>arr_c2 : C2[]
>[] : undefined[]
>[] : C2[]

var arr_i1_2: I1[] = [];
>arr_i1_2 : I1[]
>[] : undefined[]
>[] : I1[]

var arr_c1_2: C1[] = [];
>arr_c1_2 : C1[]
>[] : undefined[]
>[] : C1[]

var arr_c2_2: C2[] = [];
>arr_c2_2 : C2[]
>[] : undefined[]
>[] : C2[]

var arr_c3: C3[] = [];
>arr_c3 : C3[]
>[] : undefined[]
>[] : C3[]

// "clean up error" occurs at this point
arr_c3 = arr_c2_2; // should be an error - is
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/arrayAssignmentTest4.types
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var o1 = {one : 1};

var arr_any: any[] = [];
>arr_any : any[]
>[] : undefined[]
>[] : any[]

arr_any = function () { return null;} // should be an error - is
>arr_any = function () { return null;} : () => any
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/arrayConcat2.types
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
=== tests/cases/compiler/arrayConcat2.ts ===
var a: string[] = [];
>a : string[]
>[] : undefined[]
>[] : string[]

a.concat("hello", 'world');
>a.concat("hello", 'world') : string[]
Expand Down
44 changes: 44 additions & 0 deletions tests/baselines/reference/arrayContextualTyping.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
tests/cases/compiler/arrayContextualTyping.ts(9,9): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
tests/cases/compiler/arrayContextualTyping.ts(14,11): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.


==== tests/cases/compiler/arrayContextualTyping.ts (2 errors) ====
// Setup
declare class MyCustomArray extends Array<number> {
isCustom: true;
}

// MVP: An empty array contextually typed by an array type gets that array's type
const m1 = [] satisfies number[];
m1.push(0); // Should be OK
m1.push(""); // Should error
~~
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

// Works in object fields?
const m2 = { a: [] satisfies string[] };
m2.a.push(""); // Should be OK
m2.a.push(0); // Should error
~
!!! error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

// If the contextual type is a union, keep the array-supertyping parts of the union
const m3 = [] satisfies number[] | ArrayLike<string>;
const m3t: number[] | string[] = m3;

// Keep only the array parts
const m4 = [] satisfies MyCustomArray | string[];
const m4t: string[] = m4;

// Should OK
const m5: string[] | number[] = [] satisfies string[] | number[];
// Should OK
type Obj = { a: string[] } | { a: number[] };
const m6: Obj = { a: [] } satisfies Obj;

// Should all OK
type DiscrObj = { a: string[], kind: "strings" } | { a: number[], kind: "numbers" };
const m7: DiscrObj = { a: [], kind: "numbers"};
const m8: DiscrObj = { a: [], kind: "numbers"} satisfies DiscrObj;
const m9: DiscrObj = { a: [], kind: "strings"} satisfies DiscrObj;

59 changes: 59 additions & 0 deletions tests/baselines/reference/arrayContextualTyping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//// [arrayContextualTyping.ts]
// Setup
declare class MyCustomArray extends Array<number> {
isCustom: true;
}

// MVP: An empty array contextually typed by an array type gets that array's type
const m1 = [] satisfies number[];
m1.push(0); // Should be OK
m1.push(""); // Should error

// Works in object fields?
const m2 = { a: [] satisfies string[] };
m2.a.push(""); // Should be OK
m2.a.push(0); // Should error

// If the contextual type is a union, keep the array-supertyping parts of the union
const m3 = [] satisfies number[] | ArrayLike<string>;
const m3t: number[] | string[] = m3;

// Keep only the array parts
const m4 = [] satisfies MyCustomArray | string[];
const m4t: string[] = m4;

// Should OK
const m5: string[] | number[] = [] satisfies string[] | number[];
// Should OK
type Obj = { a: string[] } | { a: number[] };
const m6: Obj = { a: [] } satisfies Obj;

// Should all OK
type DiscrObj = { a: string[], kind: "strings" } | { a: number[], kind: "numbers" };
const m7: DiscrObj = { a: [], kind: "numbers"};
const m8: DiscrObj = { a: [], kind: "numbers"} satisfies DiscrObj;
const m9: DiscrObj = { a: [], kind: "strings"} satisfies DiscrObj;


//// [arrayContextualTyping.js]
"use strict";
// MVP: An empty array contextually typed by an array type gets that array's type
var m1 = [];
m1.push(0); // Should be OK
m1.push(""); // Should error
// Works in object fields?
var m2 = { a: [] };
m2.a.push(""); // Should be OK
m2.a.push(0); // Should error
// If the contextual type is a union, keep the array-supertyping parts of the union
var m3 = [];
var m3t = m3;
// Keep only the array parts
var m4 = [];
var m4t = m4;
// Should OK
var m5 = [];
var m6 = { a: [] };
var m7 = { a: [], kind: "numbers" };
var m8 = { a: [], kind: "numbers" };
var m9 = { a: [], kind: "strings" };
105 changes: 105 additions & 0 deletions tests/baselines/reference/arrayContextualTyping.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
=== tests/cases/compiler/arrayContextualTyping.ts ===
// Setup
declare class MyCustomArray extends Array<number> {
>MyCustomArray : Symbol(MyCustomArray, Decl(arrayContextualTyping.ts, 0, 0))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

isCustom: true;
>isCustom : Symbol(MyCustomArray.isCustom, Decl(arrayContextualTyping.ts, 1, 51))
}
Comment on lines +8 to +9
Copy link

Choose a reason for hiding this comment

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

@RyanCavanaugh Could you please clarify what is the purpose of this isCustom property? Looking at this, it seems to me like it is a condition to run a different set of tests, if so it does not seem like a good practice to have conditional tests.


// MVP: An empty array contextually typed by an array type gets that array's type
const m1 = [] satisfies number[];
>m1 : Symbol(m1, Decl(arrayContextualTyping.ts, 6, 5))

m1.push(0); // Should be OK
>m1.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
>m1 : Symbol(m1, Decl(arrayContextualTyping.ts, 6, 5))
>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))

m1.push(""); // Should error
>m1.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
>m1 : Symbol(m1, Decl(arrayContextualTyping.ts, 6, 5))
>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))

// Works in object fields?
const m2 = { a: [] satisfies string[] };
>m2 : Symbol(m2, Decl(arrayContextualTyping.ts, 11, 5))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 11, 12))

m2.a.push(""); // Should be OK
>m2.a.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
>m2.a : Symbol(a, Decl(arrayContextualTyping.ts, 11, 12))
>m2 : Symbol(m2, Decl(arrayContextualTyping.ts, 11, 5))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 11, 12))
>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))

m2.a.push(0); // Should error
>m2.a.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))
>m2.a : Symbol(a, Decl(arrayContextualTyping.ts, 11, 12))
>m2 : Symbol(m2, Decl(arrayContextualTyping.ts, 11, 5))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 11, 12))
>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --))

// If the contextual type is a union, keep the array-supertyping parts of the union
const m3 = [] satisfies number[] | ArrayLike<string>;
>m3 : Symbol(m3, Decl(arrayContextualTyping.ts, 16, 5))
>ArrayLike : Symbol(ArrayLike, Decl(lib.es5.d.ts, --, --))

const m3t: number[] | string[] = m3;
>m3t : Symbol(m3t, Decl(arrayContextualTyping.ts, 17, 5))
>m3 : Symbol(m3, Decl(arrayContextualTyping.ts, 16, 5))

// Keep only the array parts
const m4 = [] satisfies MyCustomArray | string[];
>m4 : Symbol(m4, Decl(arrayContextualTyping.ts, 20, 5))
>MyCustomArray : Symbol(MyCustomArray, Decl(arrayContextualTyping.ts, 0, 0))

const m4t: string[] = m4;
>m4t : Symbol(m4t, Decl(arrayContextualTyping.ts, 21, 5))
>m4 : Symbol(m4, Decl(arrayContextualTyping.ts, 20, 5))

// Should OK
const m5: string[] | number[] = [] satisfies string[] | number[];
>m5 : Symbol(m5, Decl(arrayContextualTyping.ts, 24, 5))

// Should OK
type Obj = { a: string[] } | { a: number[] };
>Obj : Symbol(Obj, Decl(arrayContextualTyping.ts, 24, 65))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 26, 12))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 26, 30))

const m6: Obj = { a: [] } satisfies Obj;
>m6 : Symbol(m6, Decl(arrayContextualTyping.ts, 27, 5))
>Obj : Symbol(Obj, Decl(arrayContextualTyping.ts, 24, 65))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 27, 17))
>Obj : Symbol(Obj, Decl(arrayContextualTyping.ts, 24, 65))

// Should all OK
type DiscrObj = { a: string[], kind: "strings" } | { a: number[], kind: "numbers" };
>DiscrObj : Symbol(DiscrObj, Decl(arrayContextualTyping.ts, 27, 40))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 30, 17))
>kind : Symbol(kind, Decl(arrayContextualTyping.ts, 30, 30))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 30, 52))
>kind : Symbol(kind, Decl(arrayContextualTyping.ts, 30, 65))

const m7: DiscrObj = { a: [], kind: "numbers"};
>m7 : Symbol(m7, Decl(arrayContextualTyping.ts, 31, 5))
>DiscrObj : Symbol(DiscrObj, Decl(arrayContextualTyping.ts, 27, 40))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 31, 22))
>kind : Symbol(kind, Decl(arrayContextualTyping.ts, 31, 29))

const m8: DiscrObj = { a: [], kind: "numbers"} satisfies DiscrObj;
>m8 : Symbol(m8, Decl(arrayContextualTyping.ts, 32, 5))
>DiscrObj : Symbol(DiscrObj, Decl(arrayContextualTyping.ts, 27, 40))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 32, 22))
>kind : Symbol(kind, Decl(arrayContextualTyping.ts, 32, 29))
>DiscrObj : Symbol(DiscrObj, Decl(arrayContextualTyping.ts, 27, 40))

const m9: DiscrObj = { a: [], kind: "strings"} satisfies DiscrObj;
>m9 : Symbol(m9, Decl(arrayContextualTyping.ts, 33, 5))
>DiscrObj : Symbol(DiscrObj, Decl(arrayContextualTyping.ts, 27, 40))
>a : Symbol(a, Decl(arrayContextualTyping.ts, 33, 22))
>kind : Symbol(kind, Decl(arrayContextualTyping.ts, 33, 29))
>DiscrObj : Symbol(DiscrObj, Decl(arrayContextualTyping.ts, 27, 40))

Loading