Skip to content

Commit 4609a44

Browse files
committed
Do not widen computed properties with template literal types
1 parent 5021dd8 commit 4609a44

8 files changed

+1014
-13
lines changed

src/compiler/checker.ts

+33-12
Original file line numberDiff line numberDiff line change
@@ -30099,18 +30099,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3009930099
isTypeAssignableToKind(checkComputedPropertyName(firstDecl.name), TypeFlags.ESSymbol));
3010030100
}
3010130101

30102-
function getObjectLiteralIndexInfo(node: ObjectLiteralExpression, offset: number, properties: Symbol[], keyType: Type): IndexInfo {
30102+
function getObjectLiteralIndexInfo(offset: number, properties: Symbol[], keyType: Type, isReadonly: boolean): IndexInfo {
3010330103
const propTypes: Type[] = [];
3010430104
for (let i = offset; i < properties.length; i++) {
3010530105
const prop = properties[i];
30106-
if (keyType === stringType && !isSymbolWithSymbolName(prop) ||
30107-
keyType === numberType && isSymbolWithNumericName(prop) ||
30108-
keyType === esSymbolType && isSymbolWithSymbolName(prop)) {
30106+
if (keyType === numberType && isSymbolWithNumericName(prop) || keyType === esSymbolType && isSymbolWithSymbolName(prop)) {
3010930107
propTypes.push(getTypeOfSymbol(properties[i]));
3011030108
}
30109+
else if (!isSymbolWithSymbolName(prop)) {
30110+
if (keyType === stringType) {
30111+
propTypes.push(getTypeOfSymbol(properties[i]));
30112+
}
30113+
else {
30114+
const source = tryCast(prop, isTransientSymbol)?.links.computedNameType || getStringLiteralType(unescapeLeadingUnderscores(prop.escapedName));
30115+
if (isTypeAssignableTo(source, keyType)) {
30116+
propTypes.push(getTypeOfSymbol(properties[i]));
30117+
}
30118+
}
30119+
}
3011130120
}
3011230121
const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType;
30113-
return createIndexInfo(keyType, unionType, isConstContext(node));
30122+
return createIndexInfo(keyType, unionType, isReadonly);
3011430123
}
3011530124

3011630125
function getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined {
@@ -30133,6 +30142,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3013330142
const allPropertiesTable = strictNullChecks ? createSymbolTable() : undefined;
3013430143
let propertiesTable = createSymbolTable();
3013530144
let propertiesArray: Symbol[] = [];
30145+
let computedNameTypes: Type[] = [];
3013630146
let spread: Type = emptyObjectType;
3013730147

3013830148
pushCachedContextualType(node);
@@ -30191,7 +30201,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3019130201
if (nameType) {
3019230202
prop.links.nameType = nameType;
3019330203
}
30194-
30204+
else if (computedNameType) {
30205+
prop.links.computedNameType = computedNameType;
30206+
}
3019530207
if (inDestructuringPattern) {
3019630208
// If object literal is an assignment pattern and if the assignment pattern specifies a default value
3019730209
// for the property, make the property optional.
@@ -30243,6 +30255,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3024330255
spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext);
3024430256
propertiesArray = [];
3024530257
propertiesTable = createSymbolTable();
30258+
computedNameTypes = [];
3024630259
hasComputedStringProperty = false;
3024730260
hasComputedNumberProperty = false;
3024830261
hasComputedSymbolProperty = false;
@@ -30275,8 +30288,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3027530288
checkNodeDeferred(memberDecl);
3027630289
}
3027730290

30278-
if (computedNameType && !(computedNameType.flags & TypeFlags.StringOrNumberLiteralOrUnique)) {
30279-
if (isTypeAssignableTo(computedNameType, stringNumberSymbolType)) {
30291+
if (computedNameType && !isTypeUsableAsPropertyName(computedNameType)) {
30292+
if (isPatternLiteralType(computedNameType)) {
30293+
computedNameTypes.push(computedNameType);
30294+
}
30295+
else if (isTypeAssignableTo(computedNameType, stringNumberSymbolType)) {
3028030296
if (isTypeAssignableTo(computedNameType, numberType)) {
3028130297
hasComputedNumberProperty = true;
3028230298
}
@@ -30339,6 +30355,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3033930355
spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext);
3034030356
propertiesArray = [];
3034130357
propertiesTable = createSymbolTable();
30358+
computedNameTypes = [];
3034230359
hasComputedStringProperty = false;
3034330360
hasComputedNumberProperty = false;
3034430361
}
@@ -30349,10 +30366,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3034930366
return createObjectLiteralType();
3035030367

3035130368
function createObjectLiteralType() {
30352-
const indexInfos = [];
30353-
if (hasComputedStringProperty) indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, stringType));
30354-
if (hasComputedNumberProperty) indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, numberType));
30355-
if (hasComputedSymbolProperty) indexInfos.push(getObjectLiteralIndexInfo(node, offset, propertiesArray, esSymbolType));
30369+
let indexInfos: IndexInfo[] = [];
30370+
for (const computedNameType of computedNameTypes) {
30371+
const indexInfo = getObjectLiteralIndexInfo(offset, propertiesArray, computedNameType, /*isReadonly*/ inConstContext);
30372+
indexInfos = appendIndexInfo(indexInfos, indexInfo, /*union*/ true);
30373+
}
30374+
if (hasComputedStringProperty) indexInfos.push(getObjectLiteralIndexInfo(offset, propertiesArray, stringType, /*isReadonly*/ inConstContext));
30375+
if (hasComputedNumberProperty) indexInfos.push(getObjectLiteralIndexInfo(offset, propertiesArray, numberType, /*isReadonly*/ inConstContext));
30376+
if (hasComputedSymbolProperty) indexInfos.push(getObjectLiteralIndexInfo(offset, propertiesArray, esSymbolType, /*isReadonly*/ inConstContext));
3035630377
const result = createAnonymousType(node.symbol, propertiesTable, emptyArray, emptyArray, indexInfos);
3035730378
result.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral;
3035830379
if (isJSObjectLiteral) {

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5839,6 +5839,7 @@ export interface SymbolLinks {
58395839
type?: Type; // Type of value symbol
58405840
writeType?: Type; // Type of value symbol in write contexts
58415841
nameType?: Type; // Type associated with a late-bound symbol
5842+
computedNameType?: Type; // Type of the computed property name not usable as property name
58425843
uniqueESSymbolType?: Type; // UniqueESSymbol type for a symbol
58435844
declaredType?: Type; // Type of class, interface, enum, type alias, or type parameter
58445845
typeParameters?: TypeParameter[]; // Type parameters of type alias (undefined if non-generic)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
computedPropertyNamesTemplateLiteralTypes.ts(94,7): error TS2322: Type '{ [x: string]: string | number[]; name: string; }' is not assignable to type 'IDocument_46309'.
2+
'string' and '`added_${string}`' index signatures are incompatible.
3+
Type 'string | number[]' is not assignable to type 'number[] | undefined'.
4+
Type 'string' is not assignable to type 'number[]'.
5+
6+
7+
==== computedPropertyNamesTemplateLiteralTypes.ts (1 errors) ====
8+
declare const str1: string;
9+
declare const pattern1: `foo${string}`;
10+
declare const pattern2: `foobar${string}`;
11+
declare const samepattern1: `foo${string}`;
12+
13+
const obj1 = {
14+
[pattern1]: true,
15+
};
16+
17+
const obj2 = {
18+
[pattern1]: true,
19+
[str1]: 100,
20+
};
21+
22+
const obj3 = {
23+
[str1]: 100,
24+
[pattern1]: true,
25+
};
26+
27+
const obj4 = {
28+
[pattern1]: true,
29+
[pattern2]: "hello",
30+
};
31+
32+
const obj5 = {
33+
[pattern2]: "hello",
34+
[pattern1]: true,
35+
};
36+
37+
const obj6 = {
38+
[pattern1]: true,
39+
[pattern2]: "hello",
40+
other: 100,
41+
};
42+
43+
const obj7 = {
44+
[pattern1]: true,
45+
[pattern2]: "hello",
46+
fooooooooo: 100,
47+
};
48+
49+
const obj8 = {
50+
[pattern1]: true,
51+
[pattern2]: "hello",
52+
foobarrrrr: 100,
53+
};
54+
55+
const obj9 = {
56+
[pattern1]: true,
57+
[samepattern1]: "hello",
58+
};
59+
60+
const obj10 = {
61+
[pattern1]: true,
62+
} as const;
63+
64+
const obj11 = {
65+
[pattern1]: 100,
66+
...obj9,
67+
};
68+
69+
const obj12 = {
70+
...obj9,
71+
[pattern1]: 100,
72+
};
73+
74+
const obj13 = {
75+
[pattern1]: 100,
76+
...{
77+
[pattern2]: "hello",
78+
},
79+
};
80+
81+
const obj14 = {
82+
[pattern1]: 100,
83+
...{
84+
[pattern1]: true,
85+
[pattern2]: "hello",
86+
foobarrrrr: [1, 2, 3],
87+
},
88+
};
89+
90+
// repro from https://github.com/microsoft/TypeScript/issues/46309
91+
92+
interface IDocument_46309 {
93+
name: string;
94+
[added_: `added_${string}`]: number[] | undefined;
95+
}
96+
97+
const tech1_46309 = {
98+
uuid: "70b26275-5096-4e4b-9d50-3c965c9e5073",
99+
};
100+
101+
const doc_46309: IDocument_46309 = {
102+
~~~~~~~~~
103+
!!! error TS2322: Type '{ [x: string]: string | number[]; name: string; }' is not assignable to type 'IDocument_46309'.
104+
!!! error TS2322: 'string' and '`added_${string}`' index signatures are incompatible.
105+
!!! error TS2322: Type 'string | number[]' is not assignable to type 'number[] | undefined'.
106+
!!! error TS2322: Type 'string' is not assignable to type 'number[]'.
107+
name: "",
108+
[`added_${tech1_46309.uuid}`]: [19700101],
109+
};
110+
111+
const doc2_46309: IDocument_46309 = {
112+
name: "",
113+
[`added_${tech1_46309.uuid}` as const]: [19700101],
114+
};
115+

0 commit comments

Comments
 (0)