Skip to content

Commit

Permalink
Only infer readonly tuples for const type parameters when constrain…
Browse files Browse the repository at this point in the history
…ts permit (microsoft#55229)

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
2 people authored and snovader committed Sep 23, 2023
1 parent ca01147 commit 008fb25
Show file tree
Hide file tree
Showing 6 changed files with 854 additions and 5 deletions.
16 changes: 11 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23674,6 +23674,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 @@ -24338,7 +24344,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 @@ -30541,7 +30547,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))));
}
return createArrayLiteralType(createArrayType(
elementTypes.length ?
Expand Down Expand Up @@ -33139,7 +33145,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 @@ -35455,7 +35461,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 @@ -35479,7 +35485,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

0 comments on commit 008fb25

Please sign in to comment.