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
10 changes: 5 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23925,7 +23925,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) && !isMutableArrayOrTuple(getBaseConstraintOrType(targetRestType))), targetRestType);
Copy link
Contributor

@Andarist Andarist Aug 1, 2023

Choose a reason for hiding this comment

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

IIUC this won't work correctly with union constraints, for example:

declare function invoke<const T extends [string, unknown] | [unknown, string]>(
  f: (...args: T) => void,
  ...args: T
): T;
const res = invoke((a: string, b: string) => {}, "hello", "world");

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this should probably use some(...) to distribute over unions.

}
}

Expand Down Expand Up @@ -30045,7 +30045,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 && isMutableArrayOrTuple(contextualType))));
}
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 +32623,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 && !isMutableArrayOrTuple(getBaseConstraintOrType(restType)), 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 +34928,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 +34952,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
72 changes: 72 additions & 0 deletions tests/baselines/reference/typeParameterConstModifiers.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,76 @@ 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 fn1<const T extends { foo: unknown[] }[]>(...args: T): T;

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

108 changes: 108 additions & 0 deletions tests/baselines/reference/typeParameterConstModifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,78 @@ 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 fn1<const T extends { foo: unknown[] }[]>(...args: T): T;

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


//// [typeParameterConstModifiers.js]
Expand Down Expand Up @@ -154,3 +226,39 @@ 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);
fn1({ foo: ["hello", 123] }, { foo: [true] });
Loading
Loading