Skip to content

Commit 56798a7

Browse files
authored
feat(type-info): preserve object-literal as const properties (#10143)
1 parent 09401d3 commit 56798a7

22 files changed

Lines changed: 937 additions & 74 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
[`noMisleadingReturnType`](https://biomejs.dev/linter/rules/no-misleading-return-type/) now detects misleading return type annotations when object literal properties are initialized with `as const`.
6+
7+
This function is now reported because the return annotation widens a property initialized with `as const`:
8+
9+
```ts
10+
function f(): { value: string } {
11+
return { value: "text" as const };
12+
}
13+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
[`noUselessTypeConversion`](https://biomejs.dev/linter/rules/no-useless-type-conversion/) now detects redundant conversions on object literal properties initialized with `as const`.
6+
7+
This conversion is now reported because `message.value` is inferred as a string literal:
8+
9+
```ts
10+
const message = { value: "text" as const };
11+
String(message.value);
12+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
[`useExhaustiveSwitchCases`](https://biomejs.dev/linter/rules/use-exhaustive-switch-cases/) now checks switch statements over object literal properties initialized with `as const`.
6+
7+
This switch is now reported because `status.kind` is inferred as the string literal `"ready"` but no case handles it:
8+
9+
```ts
10+
const status = { kind: "ready" as const };
11+
switch (status.kind) {}
12+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
[`useStringStartsEndsWith`](https://biomejs.dev/linter/rules/use-string-starts-ends-with/) now detects string index comparisons on object literal properties initialized with `as const`.
6+
7+
This comparison is now reported because `message.value` is inferred as a string literal:
8+
9+
```ts
10+
const message = { value: "hello" as const };
11+
message.value[0] === "h";
12+
```

crates/biome_js_analyze/src/lint/nursery/no_misleading_return_type.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ fn is_only_property_literal_widening(annotation: &Type, returns: &[Type]) -> boo
551551
{
552552
let mut index_signature_has_widening = false;
553553
let all_inferred_covered = inferred_members.iter().all(|inferred_member| {
554+
if inferred_member.is_const_asserted() {
555+
return false;
556+
}
554557
if let Some(inferred_type) = inferred.resolve(&inferred_member.ty) {
555558
if types_match(&index_signature_value_type, &inferred_type) {
556559
return true;
@@ -581,6 +584,9 @@ fn is_only_property_literal_widening(annotation: &Type, returns: &[Type]) -> boo
581584
else {
582585
return false;
583586
};
587+
if inferred_member.is_const_asserted() {
588+
return false;
589+
}
584590
match (
585591
annotated.resolve(&annotated_member.ty),
586592
inferred.resolve(&inferred_member.ty),
@@ -1489,15 +1495,9 @@ fn class_type_has_instance_shape(class: &Class) -> bool {
14891495

14901496
/// Whether a type-info member contributes instance shape.
14911497
fn type_member_affects_instance_shape(member: &biome_js_type_info::TypeMember) -> bool {
1492-
match &member.kind {
1493-
TypeMemberKind::Constructor
1494-
| TypeMemberKind::Getter(_)
1495-
| TypeMemberKind::NamedStatic(_)
1496-
| TypeMemberKind::IndexSignature(_) => false,
1497-
TypeMemberKind::CallSignature
1498-
| TypeMemberKind::Named(_)
1499-
| TypeMemberKind::NamedOptional(_) => true,
1500-
}
1498+
!member.is_static()
1499+
&& !member.is_getter()
1500+
&& !member.is_index_signature_with_ty(|_| true)
15011501
}
15021502

15031503
/// Compares non-union type pairs using a work stack. Compound types

crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,32 @@ function ternaryObjectBranch(b: boolean): object { return b ? {} : { a: 1 }; }
136136
class InheritedBase { y = 1; }
137137
class ClassWithInheritedMembers extends InheritedBase {}
138138
function inheritedClassInstance(): object { return new ClassWithInheritedMembers(); }
139+
140+
function objectPropertyAsConst(): { a: string } {
141+
return { a: "x" as const };
142+
}
143+
144+
function parenthesizedObjectPropertyAsConst(): { a: string } {
145+
return { a: ("x" as const) };
146+
}
147+
148+
function objectPropertyNegativeNumberAsConst(): { a: number } {
149+
return { a: -1 as const };
150+
}
151+
152+
function objectPropertyTupleAsConst(): { a: [number, number] } {
153+
return { a: [1, 2] as const };
154+
}
155+
156+
function objectPropertyAsConstWithSibling(): { a: string; b: number } {
157+
return { a: "x" as const, b: 1 };
158+
}
159+
160+
function nestedObjectPropertyAsConst(): { outer: { a: string } } {
161+
return { outer: { a: "x" as const } };
162+
}
163+
164+
function objectWithConstPropertyFromIdentifier(): { a: string } {
165+
const result = { a: "x" as const };
166+
return result;
167+
}

crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts.snap

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,35 @@ class InheritedBase { y = 1; }
143143
class ClassWithInheritedMembers extends InheritedBase {}
144144
function inheritedClassInstance(): object { return new ClassWithInheritedMembers(); }
145145
146+
function objectPropertyAsConst(): { a: string } {
147+
return { a: "x" as const };
148+
}
149+
150+
function parenthesizedObjectPropertyAsConst(): { a: string } {
151+
return { a: ("x" as const) };
152+
}
153+
154+
function objectPropertyNegativeNumberAsConst(): { a: number } {
155+
return { a: -1 as const };
156+
}
157+
158+
function objectPropertyTupleAsConst(): { a: [number, number] } {
159+
return { a: [1, 2] as const };
160+
}
161+
162+
function objectPropertyAsConstWithSibling(): { a: string; b: number } {
163+
return { a: "x" as const, b: 1 };
164+
}
165+
166+
function nestedObjectPropertyAsConst(): { outer: { a: string } } {
167+
return { outer: { a: "x" as const } };
168+
}
169+
170+
function objectWithConstPropertyFromIdentifier(): { a: string } {
171+
const result = { a: "x" as const };
172+
return result;
173+
}
174+
146175
```
147176
148177
# Diagnostics
@@ -1707,6 +1736,168 @@ invalid.ts:138:34 lint/nursery/noMisleadingReturnType ━━━━━━━━
17071736
> 138 │ function inheritedClassInstance(): object { return new ClassWithInheritedMembers(); }
17081737
│ ^^^^^^^^
17091738
139 │
1739+
140 │ function objectPropertyAsConst(): { a: string } {
1740+
1741+
i A wider return type hides the precise types that callers could rely on.
1742+
1743+
i Narrow the return type to match what the function actually returns.
1744+
1745+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1746+
1747+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1748+
1749+
1750+
```
1751+
1752+
```
1753+
invalid.ts:140:33 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1754+
1755+
i The return type annotation is wider than what the function actually returns.
1756+
1757+
138 │ function inheritedClassInstance(): object { return new ClassWithInheritedMembers(); }
1758+
139
1759+
> 140function objectPropertyAsConst(): { a: string } {
1760+
^^^^^^^^^^^^^^^
1761+
141return { a: "x" as const };
1762+
142 │ }
1763+
1764+
i A wider return type hides the precise types that callers could rely on.
1765+
1766+
i Narrow the return type to match what the function actually returns.
1767+
1768+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1769+
1770+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1771+
1772+
1773+
```
1774+
1775+
```
1776+
invalid.ts:144:46 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1777+
1778+
i The return type annotation is wider than what the function actually returns.
1779+
1780+
142 │ }
1781+
143 │
1782+
> 144 │ function parenthesizedObjectPropertyAsConst(): { a: string } {
1783+
^^^^^^^^^^^^^^^
1784+
145return { a: ("x" as const) };
1785+
146}
1786+
1787+
i A wider return type hides the precise types that callers could rely on.
1788+
1789+
i Narrow the return type to match what the function actually returns.
1790+
1791+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1792+
1793+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1794+
1795+
1796+
```
1797+
1798+
```
1799+
invalid.ts:148:47 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1800+
1801+
i The return type annotation is wider than what the function actually returns.
1802+
1803+
146 │ }
1804+
147 │
1805+
> 148 │ function objectPropertyNegativeNumberAsConst(): { a: number } {
1806+
^^^^^^^^^^^^^^^
1807+
149return { a: -1 as const };
1808+
150}
1809+
1810+
i A wider return type hides the precise types that callers could rely on.
1811+
1812+
i Narrow the return type to match what the function actually returns.
1813+
1814+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1815+
1816+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1817+
1818+
1819+
```
1820+
1821+
```
1822+
invalid.ts:152:38 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1823+
1824+
i The return type annotation is wider than what the function actually returns.
1825+
1826+
150 │ }
1827+
151 │
1828+
> 152 │ function objectPropertyTupleAsConst(): { a: [number, number] } {
1829+
^^^^^^^^^^^^^^^^^^^^^^^^^
1830+
153return { a: [1, 2] as const };
1831+
154}
1832+
1833+
i A wider return type hides the precise types that callers could rely on.
1834+
1835+
i Narrow the return type to match what the function actually returns.
1836+
1837+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1838+
1839+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1840+
1841+
1842+
```
1843+
1844+
```
1845+
invalid.ts:156:44 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1846+
1847+
i The return type annotation is wider than what the function actually returns.
1848+
1849+
154 │ }
1850+
155 │
1851+
> 156 │ function objectPropertyAsConstWithSibling(): { a: string; b: number } {
1852+
^^^^^^^^^^^^^^^^^^^^^^^^^^
1853+
157return { a: "x" as const, b: 1 };
1854+
158}
1855+
1856+
i A wider return type hides the precise types that callers could rely on.
1857+
1858+
i Narrow the return type to match what the function actually returns.
1859+
1860+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1861+
1862+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1863+
1864+
1865+
```
1866+
1867+
```
1868+
invalid.ts:160:39 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1869+
1870+
i The return type annotation is wider than what the function actually returns.
1871+
1872+
158 │ }
1873+
159 │
1874+
> 160 │ function nestedObjectPropertyAsConst(): { outer: { a: string } } {
1875+
^^^^^^^^^^^^^^^^^^^^^^^^^^
1876+
161return { outer: { a: "x" as const } };
1877+
162}
1878+
1879+
i A wider return type hides the precise types that callers could rely on.
1880+
1881+
i Narrow the return type to match what the function actually returns.
1882+
1883+
i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/9810 for more information or to report possible bugs.
1884+
1885+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
1886+
1887+
1888+
```
1889+
1890+
```
1891+
invalid.ts:164:49 lint/nursery/noMisleadingReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1892+
1893+
i The return type annotation is wider than what the function actually returns.
1894+
1895+
162 │ }
1896+
163 │
1897+
> 164 │ function objectWithConstPropertyFromIdentifier(): { a: string } {
1898+
^^^^^^^^^^^^^^^
1899+
165const result = { a: "x" as const };
1900+
166return result;
17101901
17111902
i A wider return type hides the precise types that callers could rely on.
17121903

crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/valid.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ function objectNoConst(): { a: string; b: string } {
6161
return { a: "x", b: "y" };
6262
}
6363

64+
function propertyLiteralWithoutAsConst(): { a: string } {
65+
return { a: "x" };
66+
}
67+
68+
function propertyAsConstExact(): { a: "x" } {
69+
return { a: "x" as const };
70+
}
71+
6472
function asConstMatch(): "hello" { return "hello" as const; }
6573

6674
function bareReturn(): void { return; }

crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/valid.ts.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ function objectNoConst(): { a: string; b: string } {
6767
return { a: "x", b: "y" };
6868
}
6969
70+
function propertyLiteralWithoutAsConst(): { a: string } {
71+
return { a: "x" };
72+
}
73+
74+
function propertyAsConstExact(): { a: "x" } {
75+
return { a: "x" as const };
76+
}
77+
7078
function asConstMatch(): "hello" { return "hello" as const; }
7179
7280
function bareReturn(): void { return; }

crates/biome_js_analyze/tests/specs/nursery/noUselessTypeConversion/invalid.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,6 @@ function reqStr(x: Required<{s?: string}>) { return String(x.s); }
7070
function reqNum(x: Required<{n?: number}>) { return Number(x.n); }
7171
function reqBig(x: Required<{b?: bigint}>) { return BigInt(x.b); }
7272
function roStr(x: Readonly<{s: string}>) { return String(x.s); }
73+
74+
const wrappedStringContainer = { value: "wrapped" as const };
75+
String(wrappedStringContainer.value);

0 commit comments

Comments
 (0)