From 24ba7eb37fde8805441da5dbcb30a8eec999d09b Mon Sep 17 00:00:00 2001 From: DukeDeSouth Date: Sat, 7 Feb 2026 12:28:11 -0500 Subject: [PATCH] Fix #13948: preserve narrow types for computed property keys with union literal types When the computed property name type in an object literal is a union of literal property name types (e.g., `'a' | 'b'`), distribute the property over the union members, creating a separate named property for each constituent type. Before this fix, `{ [key]: value }` where `key: 'a' | 'b'` would produce `{ [x: string]: V }` because `isTypeUsableAsPropertyName` rejects union types. Now it produces `{ a: V; b: V }`, consistent with how mapped types handle the same scenario. This fixes the long-standing React setState pattern: ```ts this.setState({ [key]: value }); // no longer errors ``` Co-authored-by: Cursor --- src/compiler/checker.ts | 27 ++ .../computedPropertyNamesUnionTypes.symbols | 216 +++++++++++ .../computedPropertyNamesUnionTypes.types | 344 ++++++++++++++++++ .../declarationEmitSimpleComputedNames1.js | 3 +- .../declarationEmitSimpleComputedNames1.types | 8 +- .../declarationComputedPropertyNames.d.ts | 3 +- .../computedPropertyNamesUnionTypes.ts | 74 ++++ 7 files changed, 669 insertions(+), 6 deletions(-) create mode 100644 tests/baselines/reference/computedPropertyNamesUnionTypes.symbols create mode 100644 tests/baselines/reference/computedPropertyNamesUnionTypes.types create mode 100644 tests/cases/compiler/computedPropertyNamesUnionTypes.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8fa3c4fce2a4a..41f344e668f05 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -33581,6 +33581,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags; + + // When the computed property name type is a union of literal property name types, + // distribute the property over the union members, creating a separate named property + // for each. This fixes #13948 where { [key]: value } with key: 'a' | 'b' would + // produce { [x: string]: V } instead of { a: V; b: V }. + if ( + computedNameType && (computedNameType.flags & TypeFlags.Union) && + every((computedNameType as UnionType).types, isTypeUsableAsPropertyName) + ) { + for (const constituentType of (computedNameType as UnionType).types) { + const propName = getPropertyNameFromType(constituentType as StringLiteralType | NumberLiteralType | UniqueESSymbolType); + const distributedProp = createSymbol(SymbolFlags.Property | member.flags, propName, checkFlags | CheckFlags.Late); + distributedProp.links.nameType = constituentType; + distributedProp.declarations = member.declarations; + distributedProp.parent = member.parent; + if (member.valueDeclaration) { + distributedProp.valueDeclaration = member.valueDeclaration; + } + distributedProp.links.type = type; + distributedProp.links.target = member; + propertiesTable.set(distributedProp.escapedName, distributedProp); + propertiesArray.push(distributedProp); + allPropertiesTable?.set(distributedProp.escapedName, distributedProp); + } + continue; + } + const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined; const prop = nameType ? createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) : diff --git a/tests/baselines/reference/computedPropertyNamesUnionTypes.symbols b/tests/baselines/reference/computedPropertyNamesUnionTypes.symbols new file mode 100644 index 0000000000000..d69aa3febc76d --- /dev/null +++ b/tests/baselines/reference/computedPropertyNamesUnionTypes.symbols @@ -0,0 +1,216 @@ +//// [tests/cases/compiler/computedPropertyNamesUnionTypes.ts] //// + +=== computedPropertyNamesUnionTypes.ts === +// Fixes #13948: Computed property key names should not be widened when the key +// type is a union of literal types. + +interface Person { +>Person : Symbol(Person, Decl(computedPropertyNamesUnionTypes.ts, 0, 0)) + + name: string; +>name : Symbol(Person.name, Decl(computedPropertyNamesUnionTypes.ts, 3, 18)) + + age: number; +>age : Symbol(Person.age, Decl(computedPropertyNamesUnionTypes.ts, 4, 17)) +} + +// Union of string literal keys should produce distributed named properties +function unionStringLiterals(key: 'a' | 'b', value: number) { +>unionStringLiterals : Symbol(unionStringLiterals, Decl(computedPropertyNamesUnionTypes.ts, 6, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 9, 29)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 9, 44)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 10, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 9, 29)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 9, 44)) + + obj.a; // ok +>obj.a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 10, 9)) +>a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17)) + + obj.b; // ok +>obj.b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 10, 9)) +>b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17)) +} + +// keyof should work with computed properties +function keyofComputed(key: keyof Person, value: string | number) { +>keyofComputed : Symbol(keyofComputed, Decl(computedPropertyNamesUnionTypes.ts, 13, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 16, 23)) +>Person : Symbol(Person, Decl(computedPropertyNamesUnionTypes.ts, 0, 0)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 16, 41)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 17, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 16, 23)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 16, 41)) + + obj.name; // ok +>obj.name : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 17, 9)) +>name : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17)) + + obj.age; // ok +>obj.age : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 17, 9)) +>age : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17)) +} + +// Partial assignability (React setState pattern) +declare function setState(state: Partial): void; +>setState : Symbol(setState, Decl(computedPropertyNamesUnionTypes.ts, 20, 1)) +>state : Symbol(state, Decl(computedPropertyNamesUnionTypes.ts, 23, 26)) +>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --)) +>Person : Symbol(Person, Decl(computedPropertyNamesUnionTypes.ts, 0, 0)) + +function reactSetState(key: 'name', value: string) { +>reactSetState : Symbol(reactSetState, Decl(computedPropertyNamesUnionTypes.ts, 23, 56)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 24, 23)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 24, 35)) + + setState({ [key]: value }); // should not error +>setState : Symbol(setState, Decl(computedPropertyNamesUnionTypes.ts, 20, 1)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 25, 14)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 24, 23)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 24, 35)) +} + +// Three-member union +function threeWay(key: 'x' | 'y' | 'z', value: boolean) { +>threeWay : Symbol(threeWay, Decl(computedPropertyNamesUnionTypes.ts, 26, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 29, 18)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 29, 39)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 29, 18)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 29, 39)) + + obj.x; // ok +>obj.x : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9)) +>x : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) + + obj.y; // ok +>obj.y : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9)) +>y : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) + + obj.z; // ok +>obj.z : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9)) +>z : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17)) +} + +// Number literal union +function numberLiterals(key: 0 | 1, value: string) { +>numberLiterals : Symbol(numberLiterals, Decl(computedPropertyNamesUnionTypes.ts, 34, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 37, 24)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 37, 35)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 38, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 38, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 37, 24)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 37, 35)) +} + +// Union key + fixed properties +function withFixed(key: 'a' | 'b') { +>withFixed : Symbol(withFixed, Decl(computedPropertyNamesUnionTypes.ts, 39, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 42, 19)) + + const obj = { [key]: 1, fixed: 'hello' }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 42, 19)) +>fixed : Symbol(fixed, Decl(computedPropertyNamesUnionTypes.ts, 43, 27)) + + obj.a; // ok, number +>obj.a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9)) +>a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17)) + + obj.b; // ok, number +>obj.b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9)) +>b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17)) + + obj.fixed; // ok, string +>obj.fixed : Symbol(fixed, Decl(computedPropertyNamesUnionTypes.ts, 43, 27)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9)) +>fixed : Symbol(fixed, Decl(computedPropertyNamesUnionTypes.ts, 43, 27)) +} + +// Mapped type equivalence +type Mapped = { [P in 'x' | 'y']: boolean }; +>Mapped : Symbol(Mapped, Decl(computedPropertyNamesUnionTypes.ts, 47, 1)) +>P : Symbol(P, Decl(computedPropertyNamesUnionTypes.ts, 50, 17)) + +function mappedEquivalence(key: 'x' | 'y', value: boolean) { +>mappedEquivalence : Symbol(mappedEquivalence, Decl(computedPropertyNamesUnionTypes.ts, 50, 44)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 51, 27)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 51, 42)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 52, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 52, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 51, 27)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 51, 42)) + + const mapped: Mapped = obj; // should be assignable +>mapped : Symbol(mapped, Decl(computedPropertyNamesUnionTypes.ts, 53, 9)) +>Mapped : Symbol(Mapped, Decl(computedPropertyNamesUnionTypes.ts, 47, 1)) +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 52, 9)) +} + +// Non-literal key should still produce index signature (unchanged behavior) +function dynamicKey(key: string, value: number) { +>dynamicKey : Symbol(dynamicKey, Decl(computedPropertyNamesUnionTypes.ts, 54, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 57, 20)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 57, 32)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 58, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 58, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 57, 20)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 57, 32)) + + obj.anything; // ok via index signature +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 58, 9)) +} + +// Template literal key should still produce index signature +function templateKey(key: `prefix_${string}`, value: number) { +>templateKey : Symbol(templateKey, Decl(computedPropertyNamesUnionTypes.ts, 60, 1)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 63, 21)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 63, 45)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 64, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 64, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 63, 21)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 63, 45)) +} + +// Generic extends literal union should work +function genericKey(key: K, value: number) { +>genericKey : Symbol(genericKey, Decl(computedPropertyNamesUnionTypes.ts, 65, 1)) +>K : Symbol(K, Decl(computedPropertyNamesUnionTypes.ts, 68, 20)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 68, 41)) +>K : Symbol(K, Decl(computedPropertyNamesUnionTypes.ts, 68, 20)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 68, 48)) + + const obj = { [key]: value }; +>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 69, 9)) +>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 69, 17)) +>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 68, 41)) +>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 68, 48)) +} + diff --git a/tests/baselines/reference/computedPropertyNamesUnionTypes.types b/tests/baselines/reference/computedPropertyNamesUnionTypes.types new file mode 100644 index 0000000000000..7e61d8e2cdbea --- /dev/null +++ b/tests/baselines/reference/computedPropertyNamesUnionTypes.types @@ -0,0 +1,344 @@ +//// [tests/cases/compiler/computedPropertyNamesUnionTypes.ts] //// + +=== computedPropertyNamesUnionTypes.ts === +// Fixes #13948: Computed property key names should not be widened when the key +// type is a union of literal types. + +interface Person { + name: string; +>name : string +> : ^^^^^^ + + age: number; +>age : number +> : ^^^^^^ +} + +// Union of string literal keys should produce distributed named properties +function unionStringLiterals(key: 'a' | 'b', value: number) { +>unionStringLiterals : (key: "a" | "b", value: number) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : "a" | "b" +> : ^^^^^^^^^ +>value : number +> : ^^^^^^ + + const obj = { [key]: value }; +>obj : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [key]: value } : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>[key] : number +> : ^^^^^^ +>key : "a" | "b" +> : ^^^^^^^^^ +>value : number +> : ^^^^^^ + + obj.a; // ok +>obj.a : number +> : ^^^^^^ +>obj : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : number +> : ^^^^^^ + + obj.b; // ok +>obj.b : number +> : ^^^^^^ +>obj : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>b : number +> : ^^^^^^ +} + +// keyof should work with computed properties +function keyofComputed(key: keyof Person, value: string | number) { +>keyofComputed : (key: keyof Person, value: string | number) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : keyof Person +> : ^^^^^^^^^^^^ +>value : string | number +> : ^^^^^^^^^^^^^^^ + + const obj = { [key]: value }; +>obj : { name: string | number; age: string | number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [key]: value } : { name: string | number; age: string | number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>[key] : string | number +> : ^^^^^^^^^^^^^^^ +>key : keyof Person +> : ^^^^^^^^^^^^ +>value : string | number +> : ^^^^^^^^^^^^^^^ + + obj.name; // ok +>obj.name : string | number +> : ^^^^^^^^^^^^^^^ +>obj : { name: string | number; age: string | number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>name : string | number +> : ^^^^^^^^^^^^^^^ + + obj.age; // ok +>obj.age : string | number +> : ^^^^^^^^^^^^^^^ +>obj : { name: string | number; age: string | number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>age : string | number +> : ^^^^^^^^^^^^^^^ +} + +// Partial assignability (React setState pattern) +declare function setState(state: Partial): void; +>setState : (state: Partial) => void +> : ^ ^^ ^^^^^ +>state : Partial +> : ^^^^^^^^^^^^^^^ + +function reactSetState(key: 'name', value: string) { +>reactSetState : (key: "name", value: string) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : "name" +> : ^^^^^^ +>value : string +> : ^^^^^^ + + setState({ [key]: value }); // should not error +>setState({ [key]: value }) : void +> : ^^^^ +>setState : (state: Partial) => void +> : ^ ^^ ^^^^^ +>{ [key]: value } : { name: string; } +> : ^^^^^^^^^^^^^^^^^ +>[key] : string +> : ^^^^^^ +>key : "name" +> : ^^^^^^ +>value : string +> : ^^^^^^ +} + +// Three-member union +function threeWay(key: 'x' | 'y' | 'z', value: boolean) { +>threeWay : (key: "x" | "y" | "z", value: boolean) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : "x" | "y" | "z" +> : ^^^^^^^^^^^^^^^ +>value : boolean +> : ^^^^^^^ + + const obj = { [key]: value }; +>obj : { x: boolean; y: boolean; z: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [key]: value } : { x: boolean; y: boolean; z: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>[key] : boolean +> : ^^^^^^^ +>key : "x" | "y" | "z" +> : ^^^^^^^^^^^^^^^ +>value : boolean +> : ^^^^^^^ + + obj.x; // ok +>obj.x : boolean +> : ^^^^^^^ +>obj : { x: boolean; y: boolean; z: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>x : boolean +> : ^^^^^^^ + + obj.y; // ok +>obj.y : boolean +> : ^^^^^^^ +>obj : { x: boolean; y: boolean; z: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>y : boolean +> : ^^^^^^^ + + obj.z; // ok +>obj.z : boolean +> : ^^^^^^^ +>obj : { x: boolean; y: boolean; z: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>z : boolean +> : ^^^^^^^ +} + +// Number literal union +function numberLiterals(key: 0 | 1, value: string) { +>numberLiterals : (key: 0 | 1, value: string) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : 0 | 1 +> : ^^^^^ +>value : string +> : ^^^^^^ + + const obj = { [key]: value }; +>obj : { 0: string; 1: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [key]: value } : { 0: string; 1: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>[key] : string +> : ^^^^^^ +>key : 0 | 1 +> : ^^^^^ +>value : string +> : ^^^^^^ +} + +// Union key + fixed properties +function withFixed(key: 'a' | 'b') { +>withFixed : (key: "a" | "b") => void +> : ^ ^^ ^^^^^^^^^ +>key : "a" | "b" +> : ^^^^^^^^^ + + const obj = { [key]: 1, fixed: 'hello' }; +>obj : { a: number; b: number; fixed: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [key]: 1, fixed: 'hello' } : { a: number; b: number; fixed: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>[key] : number +> : ^^^^^^ +>key : "a" | "b" +> : ^^^^^^^^^ +>1 : 1 +> : ^ +>fixed : string +> : ^^^^^^ +>'hello' : "hello" +> : ^^^^^^^ + + obj.a; // ok, number +>obj.a : number +> : ^^^^^^ +>obj : { a: number; b: number; fixed: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : number +> : ^^^^^^ + + obj.b; // ok, number +>obj.b : number +> : ^^^^^^ +>obj : { a: number; b: number; fixed: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>b : number +> : ^^^^^^ + + obj.fixed; // ok, string +>obj.fixed : string +> : ^^^^^^ +>obj : { a: number; b: number; fixed: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>fixed : string +> : ^^^^^^ +} + +// Mapped type equivalence +type Mapped = { [P in 'x' | 'y']: boolean }; +>Mapped : Mapped +> : ^^^^^^ + +function mappedEquivalence(key: 'x' | 'y', value: boolean) { +>mappedEquivalence : (key: "x" | "y", value: boolean) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : "x" | "y" +> : ^^^^^^^^^ +>value : boolean +> : ^^^^^^^ + + const obj = { [key]: value }; +>obj : { x: boolean; y: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [key]: value } : { x: boolean; y: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>[key] : boolean +> : ^^^^^^^ +>key : "x" | "y" +> : ^^^^^^^^^ +>value : boolean +> : ^^^^^^^ + + const mapped: Mapped = obj; // should be assignable +>mapped : Mapped +> : ^^^^^^ +>obj : { x: boolean; y: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} + +// Non-literal key should still produce index signature (unchanged behavior) +function dynamicKey(key: string, value: number) { +>dynamicKey : (key: string, value: number) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : string +> : ^^^^^^ +>value : number +> : ^^^^^^ + + const obj = { [key]: value }; +>obj : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>{ [key]: value } : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>[key] : number +> : ^^^^^^ +>key : string +> : ^^^^^^ +>value : number +> : ^^^^^^ + + obj.anything; // ok via index signature +>obj.anything : number +> : ^^^^^^ +>obj : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>anything : number +> : ^^^^^^ +} + +// Template literal key should still produce index signature +function templateKey(key: `prefix_${string}`, value: number) { +>templateKey : (key: `prefix_${string}`, value: number) => void +> : ^ ^^ ^^ ^^ ^^^^^^^^^ +>key : `prefix_${string}` +> : ^^^^^^^^^^^^^^^^^^ +>value : number +> : ^^^^^^ + + const obj = { [key]: value }; +>obj : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>{ [key]: value } : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>[key] : number +> : ^^^^^^ +>key : `prefix_${string}` +> : ^^^^^^^^^^^^^^^^^^ +>value : number +> : ^^^^^^ +} + +// Generic extends literal union should work +function genericKey(key: K, value: number) { +>genericKey : (key: K, value: number) => void +> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^ ^^^^^^^^^ +>key : K +> : ^ +>value : number +> : ^^^^^^ + + const obj = { [key]: value }; +>obj : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>{ [key]: value } : { [key]: number; } +> : ^^ ^^^^^^ ^^ +>[key] : number +> : ^^^^^^ +>key : K +> : ^ +>value : number +> : ^^^^^^ +} + diff --git a/tests/baselines/reference/declarationEmitSimpleComputedNames1.js b/tests/baselines/reference/declarationEmitSimpleComputedNames1.js index 317aaefcdc557..7b36b8c69e8cb 100644 --- a/tests/baselines/reference/declarationEmitSimpleComputedNames1.js +++ b/tests/baselines/reference/declarationEmitSimpleComputedNames1.js @@ -71,7 +71,8 @@ exports.instanceLookup = (new Holder())["some" + "thing"]; //// [declarationEmitSimpleComputedNames1.d.ts] export declare const fieldName: string; export declare const conatainer: { - [fieldName]: () => string; + f1(): string; + f2(): string; }; declare const classFieldName: string; declare const otherField: string; diff --git a/tests/baselines/reference/declarationEmitSimpleComputedNames1.types b/tests/baselines/reference/declarationEmitSimpleComputedNames1.types index 773301e49e9d7..dbec46d52ffbf 100644 --- a/tests/baselines/reference/declarationEmitSimpleComputedNames1.types +++ b/tests/baselines/reference/declarationEmitSimpleComputedNames1.types @@ -24,10 +24,10 @@ export const fieldName = Math.random() > 0.5 ? "f1" : "f2"; > : ^^^^ export const conatainer = { ->conatainer : { [fieldName]: () => string; } -> : ^^ ^^^^^^^^^^^^ ^^ ->{ [fieldName]() { return "result"; }} : { [fieldName]: () => string; } -> : ^^ ^^^^^^^^^^^^ ^^ +>conatainer : { f1(): string; f2(): string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>{ [fieldName]() { return "result"; }} : { f1(): string; f2(): string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [fieldName]() { >[fieldName] : () => string diff --git a/tests/baselines/reference/transpile/declarationComputedPropertyNames.d.ts b/tests/baselines/reference/transpile/declarationComputedPropertyNames.d.ts index 3c81cf66f0e84..76151d5281244 100644 --- a/tests/baselines/reference/transpile/declarationComputedPropertyNames.d.ts +++ b/tests/baselines/reference/transpile/declarationComputedPropertyNames.d.ts @@ -93,12 +93,13 @@ export declare class C { ["2"]: number; } export declare const D: { - [x: string]: number; [x: number]: number; [presentNs.a]: number; [aliasing.toStringTag]: number; 1: number; "2": number; + f1: number; + f2: number; }; export {}; diff --git a/tests/cases/compiler/computedPropertyNamesUnionTypes.ts b/tests/cases/compiler/computedPropertyNamesUnionTypes.ts new file mode 100644 index 0000000000000..5f1278c7e0bf8 --- /dev/null +++ b/tests/cases/compiler/computedPropertyNamesUnionTypes.ts @@ -0,0 +1,74 @@ +// @strict: true +// @noEmit: true + +// Fixes #13948: Computed property key names should not be widened when the key +// type is a union of literal types. + +interface Person { + name: string; + age: number; +} + +// Union of string literal keys should produce distributed named properties +function unionStringLiterals(key: 'a' | 'b', value: number) { + const obj = { [key]: value }; + obj.a; // ok + obj.b; // ok +} + +// keyof should work with computed properties +function keyofComputed(key: keyof Person, value: string | number) { + const obj = { [key]: value }; + obj.name; // ok + obj.age; // ok +} + +// Partial assignability (React setState pattern) +declare function setState(state: Partial): void; +function reactSetState(key: 'name', value: string) { + setState({ [key]: value }); // should not error +} + +// Three-member union +function threeWay(key: 'x' | 'y' | 'z', value: boolean) { + const obj = { [key]: value }; + obj.x; // ok + obj.y; // ok + obj.z; // ok +} + +// Number literal union +function numberLiterals(key: 0 | 1, value: string) { + const obj = { [key]: value }; +} + +// Union key + fixed properties +function withFixed(key: 'a' | 'b') { + const obj = { [key]: 1, fixed: 'hello' }; + obj.a; // ok, number + obj.b; // ok, number + obj.fixed; // ok, string +} + +// Mapped type equivalence +type Mapped = { [P in 'x' | 'y']: boolean }; +function mappedEquivalence(key: 'x' | 'y', value: boolean) { + const obj = { [key]: value }; + const mapped: Mapped = obj; // should be assignable +} + +// Non-literal key should still produce index signature (unchanged behavior) +function dynamicKey(key: string, value: number) { + const obj = { [key]: value }; + obj.anything; // ok via index signature +} + +// Template literal key should still produce index signature +function templateKey(key: `prefix_${string}`, value: number) { + const obj = { [key]: value }; +} + +// Generic extends literal union should work +function genericKey(key: K, value: number) { + const obj = { [key]: value }; +}