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

Only infer readonly tuples for const type parameters when constraints permit #55229

Merged
merged 10 commits into from
Aug 26, 2023
16 changes: 11 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23265,6 +23265,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType);
}

function isMutableArrayLikeType(type: Type): boolean {
// A type is mutable-array-like if it is a reference to the global Array type, or if it is not the
// any, undefined or null type and if it is assignable to Array<any>
return isMutableArrayOrTuple(type) || !(type.flags & (TypeFlags.Any | TypeFlags.Nullable)) && isTypeAssignableTo(type, anyArrayType);
}

function getSingleBaseForNonAugmentingSubtype(type: Type) {
if (!(getObjectFlags(type) & ObjectFlags.Reference) || !(getObjectFlags((type as TypeReference).target) & ObjectFlags.ClassOrInterface)) {
return undefined;
Expand Down Expand Up @@ -23925,7 +23931,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
callback(getTypeAtPosition(source, i), getTypeAtPosition(target, i));
}
if (targetRestType) {
callback(getRestTypeAtPosition(source, paramCount), targetRestType);
callback(getRestTypeAtPosition(source, paramCount, /*readonly*/ isConstTypeVariable(targetRestType) && !someType(targetRestType, isMutableArrayLikeType)), targetRestType);
}
}

Expand Down Expand Up @@ -30045,7 +30051,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return createTupleType(elementTypes, elementFlags);
}
if (forceTuple || inConstContext || inTupleContext) {
return createArrayLiteralType(createTupleType(elementTypes, elementFlags, /*readonly*/ inConstContext));
return createArrayLiteralType(createTupleType(elementTypes, elementFlags, /*readonly*/ inConstContext && !(contextualType && someType(contextualType, isMutableArrayLikeType))));
Copy link
Contributor

Choose a reason for hiding this comment

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

This strategy doesn't seem to cover for a case like this:

declare function test_mix<const T extends [number, number] | readonly [string, string]>(a: T): T

// actual: [number, number] | readonly [string, string];
// expected: [1, 2]
const mix1 = test_mix([1, 2]); 

Copy link
Member Author

Choose a reason for hiding this comment

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

With latest commits we now infer a mutable array type if any of the array types in a union type constraint are mutable.

}
return createArrayLiteralType(createArrayType(elementTypes.length ?
getUnionType(sameMap(elementTypes, (t, i) => elementFlags[i] & ElementFlags.Variadic ? getIndexedAccessTypeOrUndefined(t, numberType) || anyType : t), UnionReduction.Subtype) :
Expand Down Expand Up @@ -32623,7 +32629,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
names.push((arg as SyntheticExpression).tupleNameSource!);
}
}
return createTupleType(types, flags, inConstContext, length(names) === length(types) ? names : undefined);
return createTupleType(types, flags, inConstContext && !someType(restType, isMutableArrayLikeType), length(names) === length(types) ? names : undefined);
}

function checkTypeArguments(signature: Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage): Type[] | undefined {
Expand Down Expand Up @@ -34928,7 +34934,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}

function getRestTypeAtPosition(source: Signature, pos: number): Type {
function getRestTypeAtPosition(source: Signature, pos: number, readonly?: boolean): Type {
const parameterCount = getParameterCount(source);
const minArgumentCount = getMinArgumentCount(source);
const restType = getEffectiveRestType(source);
Expand All @@ -34952,7 +34958,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
names.push(name);
}
}
return createTupleType(types, flags, /*readonly*/ false, length(names) === length(types) ? names : undefined);
return createTupleType(types, flags, readonly, length(names) === length(types) ? names : undefined);
}

// Return the number of parameters in a signature. The rest parameter, if present, counts as one
Expand Down
83 changes: 83 additions & 0 deletions tests/baselines/reference/typeParameterConstModifiers.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,87 @@ typeParameterConstModifiers.ts(55,9): error TS1277: 'const' modifier can only ap
const thingMapped = <const O extends Record<string, any>>(o: NotEmptyMapped<O>) => o;

const tMapped = thingMapped({ foo: '' }); // { foo: "" }

// repro from https://github.com/microsoft/TypeScript/issues/55033

function factory_55033_minimal<const T extends readonly unknown[]>(cb: (...args: T) => void) {
return {} as T
}

const test_55033_minimal = factory_55033_minimal((b: string) => {})

function factory_55033<const T extends readonly unknown[]>(cb: (...args: T) => void) {
return function call<const K extends T>(...args: K): K {
return {} as K;
};
}

const t1_55033 = factory_55033((a: { test: number }, b: string) => {})(
{ test: 123 },
"some string"
);

const t2_55033 = factory_55033((a: { test: number }, b: string) => {})(
{ test: 123 } as const,
"some string"
);

// Same with non-readonly constraint

function factory_55033_2<const T extends unknown[]>(cb: (...args: T) => void) {
return function call<const K extends T>(...args: K): K {
return {} as K;
};
}

const t1_55033_2 = factory_55033_2((a: { test: number }, b: string) => {})(
{ test: 123 },
"some string"
);

const t2_55033_2 = factory_55033_2((a: { test: number }, b: string) => {})(
{ test: 123 } as const,
"some string"
);

// Repro from https://github.com/microsoft/TypeScript/issues/51931

declare function fn<const T extends any[]>(...args: T): T;

const a = fn("a", false);

// More examples of non-readonly constraints

declare function fa1<const T extends unknown[]>(args: T): T;
declare function fa2<const T extends readonly unknown[]>(args: T): T;

fa1(["hello", 42]);
fa2(["hello", 42]);

declare function fb1<const T extends unknown[]>(...args: T): T;
declare function fb2<const T extends readonly unknown[]>(...args: T): T;

fb1("hello", 42);
fb2("hello", 42);

declare function fc1<const T extends unknown[]>(f: (...args: T) => void, ...args: T): T;
declare function fc2<const T extends readonly unknown[]>(f: (...args: T) => void, ...args: T): T;

fc1((a: string, b: number) => {}, "hello", 42);
fc2((a: string, b: number) => {}, "hello", 42);

declare function fd1<const T extends string[] | number[]>(args: T): T;
declare function fd2<const T extends string[] | readonly number[]>(args: T): T;
declare function fd3<const T extends readonly string[] | readonly number[]>(args: T): T;

fd1(["hello", "world"]);
fd1([1, 2, 3]);
fd2(["hello", "world"]);
fd2([1, 2, 3]);
fd3(["hello", "world"]);
fd3([1, 2, 3]);

declare function fn1<const T extends { foo: unknown[] }[]>(...args: T): T;

fn1({ foo: ["hello", 123] }, { foo: [true]});

125 changes: 125 additions & 0 deletions tests/baselines/reference/typeParameterConstModifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,89 @@ type NotEmptyMapped<T extends Record<string, any>> = keyof T extends never ? nev
const thingMapped = <const O extends Record<string, any>>(o: NotEmptyMapped<O>) => o;

const tMapped = thingMapped({ foo: '' }); // { foo: "" }

// repro from https://github.com/microsoft/TypeScript/issues/55033

function factory_55033_minimal<const T extends readonly unknown[]>(cb: (...args: T) => void) {
return {} as T
}

const test_55033_minimal = factory_55033_minimal((b: string) => {})

function factory_55033<const T extends readonly unknown[]>(cb: (...args: T) => void) {
return function call<const K extends T>(...args: K): K {
return {} as K;
};
}

const t1_55033 = factory_55033((a: { test: number }, b: string) => {})(
{ test: 123 },
"some string"
);

const t2_55033 = factory_55033((a: { test: number }, b: string) => {})(
{ test: 123 } as const,
"some string"
);

// Same with non-readonly constraint

function factory_55033_2<const T extends unknown[]>(cb: (...args: T) => void) {
return function call<const K extends T>(...args: K): K {
return {} as K;
};
}

const t1_55033_2 = factory_55033_2((a: { test: number }, b: string) => {})(
{ test: 123 },
"some string"
);

const t2_55033_2 = factory_55033_2((a: { test: number }, b: string) => {})(
{ test: 123 } as const,
"some string"
);

// Repro from https://github.com/microsoft/TypeScript/issues/51931

declare function fn<const T extends any[]>(...args: T): T;

const a = fn("a", false);

// More examples of non-readonly constraints

declare function fa1<const T extends unknown[]>(args: T): T;
declare function fa2<const T extends readonly unknown[]>(args: T): T;

fa1(["hello", 42]);
fa2(["hello", 42]);

declare function fb1<const T extends unknown[]>(...args: T): T;
declare function fb2<const T extends readonly unknown[]>(...args: T): T;

fb1("hello", 42);
fb2("hello", 42);

declare function fc1<const T extends unknown[]>(f: (...args: T) => void, ...args: T): T;
declare function fc2<const T extends readonly unknown[]>(f: (...args: T) => void, ...args: T): T;

fc1((a: string, b: number) => {}, "hello", 42);
fc2((a: string, b: number) => {}, "hello", 42);

declare function fd1<const T extends string[] | number[]>(args: T): T;
declare function fd2<const T extends string[] | readonly number[]>(args: T): T;
declare function fd3<const T extends readonly string[] | readonly number[]>(args: T): T;

fd1(["hello", "world"]);
fd1([1, 2, 3]);
fd2(["hello", "world"]);
fd2([1, 2, 3]);
fd3(["hello", "world"]);
fd3([1, 2, 3]);

declare function fn1<const T extends { foo: unknown[] }[]>(...args: T): T;

fn1({ foo: ["hello", 123] }, { foo: [true]});


//// [typeParameterConstModifiers.js]
Expand Down Expand Up @@ -154,3 +237,45 @@ var thing = function (o) { return o; };
var t = thing({ foo: '' }); // readonly { foo: "" }
var thingMapped = function (o) { return o; };
var tMapped = thingMapped({ foo: '' }); // { foo: "" }
// repro from https://github.com/microsoft/TypeScript/issues/55033
function factory_55033_minimal(cb) {
return {};
}
var test_55033_minimal = factory_55033_minimal(function (b) { });
function factory_55033(cb) {
return function call() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return {};
};
}
var t1_55033 = factory_55033(function (a, b) { })({ test: 123 }, "some string");
var t2_55033 = factory_55033(function (a, b) { })({ test: 123 }, "some string");
// Same with non-readonly constraint
function factory_55033_2(cb) {
return function call() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
return {};
};
}
var t1_55033_2 = factory_55033_2(function (a, b) { })({ test: 123 }, "some string");
var t2_55033_2 = factory_55033_2(function (a, b) { })({ test: 123 }, "some string");
var a = fn("a", false);
fa1(["hello", 42]);
fa2(["hello", 42]);
fb1("hello", 42);
fb2("hello", 42);
fc1(function (a, b) { }, "hello", 42);
fc2(function (a, b) { }, "hello", 42);
fd1(["hello", "world"]);
fd1([1, 2, 3]);
fd2(["hello", "world"]);
fd2([1, 2, 3]);
fd3(["hello", "world"]);
fd3([1, 2, 3]);
fn1({ foo: ["hello", 123] }, { foo: [true] });
Loading