eslint-config-typed 4.0.6 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/configs/typescript.d.mts.map +1 -1
  2. package/dist/configs/typescript.mjs +2 -0
  3. package/dist/configs/typescript.mjs.map +1 -1
  4. package/dist/plugins/react-coding-style/rules/display-name.d.mts +2 -2
  5. package/dist/plugins/react-coding-style/rules/display-name.d.mts.map +1 -1
  6. package/dist/plugins/react-coding-style/rules/display-name.mjs +110 -30
  7. package/dist/plugins/react-coding-style/rules/display-name.mjs.map +1 -1
  8. package/dist/plugins/react-coding-style/rules/rules.d.mts +2 -2
  9. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs +18 -9
  10. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs.map +1 -1
  11. package/dist/rules/eslint-react-coding-style-rules.d.mts +1 -3
  12. package/dist/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
  13. package/dist/rules/eslint-react-coding-style-rules.mjs +1 -1
  14. package/dist/rules/eslint-react-coding-style-rules.mjs.map +1 -1
  15. package/dist/rules/eslint-react-rules.d.mts +1 -1
  16. package/dist/rules/eslint-react-rules.mjs +1 -1
  17. package/dist/rules/eslint-react-rules.mjs.map +1 -1
  18. package/dist/types/rules/eslint-react-coding-style-rules.d.mts +21 -6
  19. package/dist/types/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
  20. package/package.json +11 -9
  21. package/src/configs/typescript.mts +4 -0
  22. package/src/plugins/react-coding-style/README.md +4 -3
  23. package/src/plugins/react-coding-style/rules/display-name.mts +160 -38
  24. package/src/plugins/react-coding-style/rules/display-name.test.mts +70 -6
  25. package/src/plugins/react-coding-style/rules/shared.test.mts +148 -0
  26. package/src/plugins/react-coding-style/rules/use-memo-hooks-style-named.test.mts +47 -11
  27. package/src/plugins/react-coding-style/rules/use-memo-hooks-style-namespace.test.mts +47 -11
  28. package/src/plugins/react-coding-style/rules/use-memo-hooks-style.mts +24 -9
  29. package/src/rules/eslint-react-coding-style-rules.mts +1 -1
  30. package/src/rules/eslint-react-rules.mts +1 -1
  31. package/src/types/rules/eslint-react-coding-style-rules.mts +21 -6
@@ -3,16 +3,16 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { expectType } from 'ts-data-forge';
6
+ import { castDeepMutable } from 'ts-data-forge';
7
7
  import { isReactApiCall } from './shared.mjs';
8
8
 
9
9
  type Options = readonly [
10
10
  Readonly<{
11
- ignoreTranspilerName?: boolean;
11
+ ignoreName?: string | readonly string[];
12
12
  }>?,
13
13
  ];
14
14
 
15
- type MessageIds = 'missingDisplayName';
15
+ type MessageIds = 'missingDisplayName' | 'mismatchedDisplayName';
16
16
 
17
17
  /**
18
18
  * Rule to require displayName property for React components
@@ -23,16 +23,23 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
23
23
  type: 'suggestion',
24
24
  docs: {
25
25
  description:
26
- 'Require displayName property for React components created with React.memo',
26
+ 'Require React.memo components to define displayName matching the component name',
27
27
  },
28
28
  schema: [
29
29
  {
30
30
  type: 'object',
31
31
  properties: {
32
- ignoreTranspilerName: {
33
- type: 'boolean',
32
+ ignoreName: {
34
33
  description:
35
- 'When true, ignores components that get displayName from variable name',
34
+ 'Component names allowed to have displayName different from the variable name.',
35
+ oneOf: [
36
+ { type: 'string' },
37
+ {
38
+ type: 'array',
39
+ items: { type: 'string' },
40
+ minItems: 0,
41
+ },
42
+ ],
36
43
  },
37
44
  },
38
45
  additionalProperties: false,
@@ -41,12 +48,17 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
41
48
  messages: {
42
49
  missingDisplayName:
43
50
  'Component should have a displayName property for better debugging',
51
+ mismatchedDisplayName:
52
+ 'displayName should match the component name "{{componentName}}"',
44
53
  },
45
54
  },
46
55
  create: (context) => {
47
56
  const options = context.options[0] ?? {};
48
57
 
49
- const ignoreTranspilerName = options.ignoreTranspilerName ?? false;
58
+ const ignoreNameSet = normalizeNames(options.ignoreName);
59
+
60
+ const shouldIgnoreMismatch = (componentName: string): boolean =>
61
+ ignoreNameSet.has(componentName);
50
62
 
51
63
  const checkComponent = (
52
64
  node: DeepReadonly<TSESTree.VariableDeclarator>,
@@ -65,48 +77,47 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
65
77
 
66
78
  const componentName = node.id.name;
67
79
 
68
- if (ignoreTranspilerName) {
69
- return;
70
- }
80
+ const assignment = getDisplayNameAssignment(node);
71
81
 
72
- const parent = node.parent;
82
+ if (assignment === undefined) {
83
+ context.report({
84
+ node: castDeepMutable(node),
85
+ messageId: 'missingDisplayName',
86
+ });
73
87
 
74
- expectType<typeof parent.type, AST_NODE_TYPES.VariableDeclaration>('=');
88
+ return;
89
+ }
75
90
 
76
- const grandParent = parent.parent;
91
+ if (!isComponentDisplayNameAssignment(assignment, componentName)) {
92
+ context.report({
93
+ node: castDeepMutable(node),
94
+ messageId: 'missingDisplayName',
95
+ });
77
96
 
78
- if (grandParent.type !== AST_NODE_TYPES.Program) {
79
97
  return;
80
98
  }
81
99
 
82
- const program = grandParent;
100
+ const displayName = extractDisplayName(assignment.right);
83
101
 
84
- const componentIndex = program.body.indexOf(parent);
102
+ if (displayName === undefined) {
103
+ context.report({
104
+ node: assignment.right,
105
+ messageId: 'mismatchedDisplayName',
106
+ data: { componentName },
107
+ });
108
+
109
+ return;
110
+ }
85
111
 
86
- if (componentIndex === -1) {
112
+ if (shouldIgnoreMismatch(componentName)) {
87
113
  return;
88
114
  }
89
115
 
90
- const nextStatement = program.body[componentIndex + 1];
91
-
92
- const hasDisplayName =
93
- nextStatement !== undefined &&
94
- nextStatement.type === AST_NODE_TYPES.ExpressionStatement &&
95
- nextStatement.expression.type === AST_NODE_TYPES.AssignmentExpression &&
96
- nextStatement.expression.left.type ===
97
- AST_NODE_TYPES.MemberExpression &&
98
- nextStatement.expression.left.object.type ===
99
- AST_NODE_TYPES.Identifier &&
100
- nextStatement.expression.left.object.name === componentName &&
101
- nextStatement.expression.left.property.type ===
102
- AST_NODE_TYPES.Identifier &&
103
- nextStatement.expression.left.property.name === 'displayName';
104
-
105
- if (!hasDisplayName) {
116
+ if (displayName !== componentName) {
106
117
  context.report({
107
- // eslint-disable-next-line total-functions/no-unsafe-type-assertion
108
- node: node.id as never,
109
- messageId: 'missingDisplayName',
118
+ node: assignment.right,
119
+ messageId: 'mismatchedDisplayName',
120
+ data: { componentName },
110
121
  });
111
122
  }
112
123
  };
@@ -115,5 +126,116 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
115
126
  VariableDeclarator: checkComponent,
116
127
  };
117
128
  },
118
- defaultOptions: [{ ignoreTranspilerName: false }],
129
+ defaultOptions: [{ ignoreName: [] }],
130
+ };
131
+
132
+ const normalizeNames = (
133
+ names: string | readonly string[] | undefined,
134
+ ): ReadonlySet<string> => {
135
+ if (names === undefined) {
136
+ return new Set();
137
+ }
138
+
139
+ if (typeof names === 'string') {
140
+ return new Set([names]);
141
+ }
142
+
143
+ return new Set(names);
144
+ };
145
+
146
+ const getDisplayNameAssignment = (
147
+ node: DeepReadonly<TSESTree.VariableDeclarator>,
148
+ ): DeepReadonly<TSESTree.AssignmentExpression> | undefined => {
149
+ let mut_current = node.parent as DeepReadonly<TSESTree.Node> | undefined;
150
+
151
+ let mut_statement: DeepReadonly<TSESTree.Statement> | undefined = undefined;
152
+
153
+ while (mut_current !== undefined) {
154
+ if (
155
+ mut_current.type === AST_NODE_TYPES.VariableDeclaration ||
156
+ mut_current.type === AST_NODE_TYPES.ExportNamedDeclaration
157
+ ) {
158
+ mut_statement = mut_current as DeepReadonly<TSESTree.Statement>;
159
+ }
160
+
161
+ if (mut_current.type === AST_NODE_TYPES.Program) {
162
+ break;
163
+ }
164
+
165
+ mut_current = mut_current.parent;
166
+ }
167
+
168
+ if (mut_current === undefined || mut_statement === undefined) {
169
+ return undefined;
170
+ }
171
+
172
+ const program = mut_current;
173
+
174
+ const componentIndex = program.body.indexOf(
175
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
176
+ mut_statement as TSESTree.Statement,
177
+ );
178
+
179
+ if (componentIndex === -1) {
180
+ return undefined;
181
+ }
182
+
183
+ const nextStatement = program.body[componentIndex + 1];
184
+
185
+ if (nextStatement === undefined) {
186
+ return undefined;
187
+ }
188
+
189
+ if (nextStatement.type !== AST_NODE_TYPES.ExpressionStatement) {
190
+ return undefined;
191
+ }
192
+
193
+ if (nextStatement.expression.type !== AST_NODE_TYPES.AssignmentExpression) {
194
+ return undefined;
195
+ }
196
+
197
+ return nextStatement.expression;
198
+ };
199
+
200
+ const isComponentDisplayNameAssignment = (
201
+ assignment: DeepReadonly<TSESTree.AssignmentExpression>,
202
+ componentName: string,
203
+ ): assignment is TSESTree.AssignmentExpression => {
204
+ if (assignment.left.type !== AST_NODE_TYPES.MemberExpression) {
205
+ return false;
206
+ }
207
+
208
+ if (assignment.left.object.type !== AST_NODE_TYPES.Identifier) {
209
+ return false;
210
+ }
211
+
212
+ if (assignment.left.object.name !== componentName) {
213
+ return false;
214
+ }
215
+
216
+ return (
217
+ assignment.left.property.type === AST_NODE_TYPES.Identifier &&
218
+ assignment.left.property.name === 'displayName'
219
+ );
220
+ };
221
+
222
+ const extractDisplayName = (
223
+ expression: DeepReadonly<TSESTree.Expression>,
224
+ ): string | undefined => {
225
+ if (
226
+ expression.type === AST_NODE_TYPES.Literal &&
227
+ typeof expression.value === 'string'
228
+ ) {
229
+ return expression.value;
230
+ }
231
+
232
+ if (
233
+ expression.type === AST_NODE_TYPES.TemplateLiteral &&
234
+ expression.expressions.length === 0 &&
235
+ expression.quasis.length === 1
236
+ ) {
237
+ return expression.quasis[0]?.value.cooked ?? undefined;
238
+ }
239
+
240
+ return undefined;
119
241
  };
@@ -41,6 +41,13 @@ describe('display-name', () => {
41
41
  const notAComponent = someFunction();
42
42
  `,
43
43
  },
44
+ {
45
+ name: 'Exported component with displayName',
46
+ code: dedent`
47
+ export const MyComponent = React.memo(() => <div>Hello</div>);
48
+ MyComponent.displayName = 'MyComponent';
49
+ `,
50
+ },
44
51
  ],
45
52
  invalid: [
46
53
  {
@@ -50,6 +57,26 @@ describe('display-name', () => {
50
57
  `,
51
58
  errors: [{ messageId: 'missingDisplayName' }],
52
59
  },
60
+ {
61
+ name: 'Exported component without displayName',
62
+ code: dedent`
63
+ export const MyComponent = React.memo(() => <div>Hello</div>);
64
+ `,
65
+ errors: [{ messageId: 'missingDisplayName' }],
66
+ },
67
+ {
68
+ name: 'Component with mismatched displayName',
69
+ code: dedent`
70
+ const MyComponent = React.memo(() => <div>Hello</div>);
71
+ MyComponent.displayName = 'Other';
72
+ `,
73
+ errors: [
74
+ {
75
+ messageId: 'mismatchedDisplayName',
76
+ data: { componentName: 'MyComponent' },
77
+ },
78
+ ],
79
+ },
53
80
  {
54
81
  name: 'Named import without displayName',
55
82
  code: dedent`
@@ -58,19 +85,47 @@ describe('display-name', () => {
58
85
  `,
59
86
  errors: [{ messageId: 'missingDisplayName' }],
60
87
  },
88
+ {
89
+ name: 'Named import with mismatched displayName',
90
+ code: dedent`
91
+ import { memo } from 'react';
92
+ const MyComponent = memo(() => <div>Hello</div>);
93
+ MyComponent.displayName = 'Component';
94
+ `,
95
+ errors: [
96
+ {
97
+ messageId: 'mismatchedDisplayName',
98
+ data: { componentName: 'MyComponent' },
99
+ },
100
+ ],
101
+ },
102
+ {
103
+ name: 'Exported component with mismatched displayName',
104
+ code: dedent`
105
+ export const MyComponent = React.memo(() => <div>Hello</div>);
106
+ MyComponent.displayName = 'Component';
107
+ `,
108
+ errors: [
109
+ {
110
+ messageId: 'mismatchedDisplayName',
111
+ data: { componentName: 'MyComponent' },
112
+ },
113
+ ],
114
+ },
61
115
  ],
62
116
  });
63
117
  });
64
118
 
65
- describe('ignoreTranspilerName option', () => {
66
- tester.run('display-name with ignoreTranspilerName', displayNameRule, {
119
+ describe('ignoreName option', () => {
120
+ tester.run('display-name with ignoreName', displayNameRule, {
67
121
  valid: [
68
122
  {
69
- name: 'Component without displayName (ignored)',
123
+ name: 'Component with mismatched displayName (ignored)',
70
124
  code: dedent`
71
125
  const MyComponent = React.memo(() => <div>Hello</div>);
126
+ MyComponent.displayName = 'Other';
72
127
  `,
73
- options: [{ ignoreTranspilerName: true }],
128
+ options: [{ ignoreName: 'MyComponent' }],
74
129
  },
75
130
  {
76
131
  name: 'Component with displayName',
@@ -78,10 +133,19 @@ describe('display-name', () => {
78
133
  const MyComponent = React.memo(() => <div>Hello</div>);
79
134
  MyComponent.displayName = 'MyComponent';
80
135
  `,
81
- options: [{ ignoreTranspilerName: true }],
136
+ options: [{ ignoreName: ['MyComponent'] }],
137
+ },
138
+ ],
139
+ invalid: [
140
+ {
141
+ name: 'Component without displayName is still reported',
142
+ code: dedent`
143
+ const MyComponent = React.memo(() => <div>Hello</div>);
144
+ `,
145
+ options: [{ ignoreName: ['MyComponent'] }],
146
+ errors: [{ messageId: 'missingDisplayName' }],
82
147
  },
83
148
  ],
84
- invalid: [],
85
149
  });
86
150
  });
87
151
  });
@@ -0,0 +1,148 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import { type TSESLint } from '@typescript-eslint/utils';
4
+ import { getReactMemoArrowFunction, isReactApiCall } from './shared.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ ecmaFeatures: { jsx: true },
13
+ },
14
+ },
15
+ });
16
+
17
+ const reactApiRule: TSESLint.RuleModule<'reactApiDetected', readonly []> = {
18
+ meta: {
19
+ type: 'problem',
20
+ docs: { description: 'test helper isReactApiCall' },
21
+ schema: [],
22
+ messages: {
23
+ reactApiDetected: 'React API call detected',
24
+ },
25
+ },
26
+ defaultOptions: [],
27
+ create: (context) => ({
28
+ CallExpression: (node) => {
29
+ if (isReactApiCall(context, node, 'memo')) {
30
+ context.report({ node, messageId: 'reactApiDetected' });
31
+ }
32
+ },
33
+ }),
34
+ };
35
+
36
+ const reactMemoArrowRule: TSESLint.RuleModule<'arrowDetected', readonly []> = {
37
+ meta: {
38
+ type: 'problem',
39
+ docs: { description: 'test helper getReactMemoArrowFunction' },
40
+ schema: [],
41
+ messages: {
42
+ arrowDetected: 'React.memo received arrow function',
43
+ },
44
+ },
45
+ defaultOptions: [],
46
+ create: (context) => ({
47
+ CallExpression: (node) => {
48
+ if (!isReactApiCall(context, node, 'memo')) {
49
+ return;
50
+ }
51
+
52
+ const arrow = getReactMemoArrowFunction(node);
53
+
54
+ if (arrow !== undefined) {
55
+ assert.strictEqual(arrow.type, 'ArrowFunctionExpression');
56
+
57
+ context.report({ node, messageId: 'arrowDetected' });
58
+ }
59
+ },
60
+ }),
61
+ };
62
+
63
+ describe('shared helpers', () => {
64
+ tester.run('isReactApiCall', reactApiRule, {
65
+ valid: [
66
+ {
67
+ name: 'non React call',
68
+ code: 'const x = fn();',
69
+ },
70
+ {
71
+ name: 'memo imported from non-react',
72
+ code: `
73
+ import { memo } from 'not-react';
74
+ const Component = memo(() => null);
75
+ `,
76
+ },
77
+ {
78
+ name: 'React member but different method',
79
+ code: `
80
+ import * as React from 'react';
81
+ const Component = React.useMemo(() => null, []);
82
+ `,
83
+ },
84
+ ],
85
+ invalid: [
86
+ {
87
+ name: 'named memo import from react',
88
+ code: `
89
+ import { memo } from 'react';
90
+ const Component = memo(() => null);
91
+ `,
92
+ errors: [{ messageId: 'reactApiDetected' }],
93
+ },
94
+ {
95
+ name: 'namespace React memo call',
96
+ code: `
97
+ import * as React from 'react';
98
+ const Component = React.memo(() => null);
99
+ `,
100
+ errors: [{ messageId: 'reactApiDetected' }],
101
+ },
102
+ {
103
+ name: 'global React memo call without import',
104
+ code: `
105
+ const Component = React.memo(() => null);
106
+ `,
107
+ errors: [{ messageId: 'reactApiDetected' }],
108
+ },
109
+ ],
110
+ });
111
+
112
+ tester.run('getReactMemoArrowFunction', reactMemoArrowRule, {
113
+ valid: [
114
+ {
115
+ name: 'memo with non-arrow first argument',
116
+ code: `
117
+ import { memo } from 'react';
118
+ function Component() { return null; }
119
+ const Wrapped = memo(Component);
120
+ `,
121
+ },
122
+ {
123
+ name: 'non React call with arrow argument',
124
+ code: `
125
+ const Wrapped = wrap(() => null);
126
+ `,
127
+ },
128
+ ],
129
+ invalid: [
130
+ {
131
+ name: 'memo with arrow function argument',
132
+ code: `
133
+ import { memo } from 'react';
134
+ const Wrapped = memo(() => null);
135
+ `,
136
+ errors: [{ messageId: 'arrowDetected' }],
137
+ },
138
+ {
139
+ name: 'React namespace memo with arrow function argument',
140
+ code: `
141
+ import * as React from 'react';
142
+ const Wrapped = React.memo(() => null);
143
+ `,
144
+ errors: [{ messageId: 'arrowDetected' }],
145
+ },
146
+ ],
147
+ });
148
+ });
@@ -28,12 +28,14 @@ describe('use-memo-hooks-style', () => {
28
28
  import { memo, useMemo } from 'react';
29
29
 
30
30
  type Props = Readonly<{
31
- readonly value: number;
31
+ value: number;
32
32
  }>;
33
33
 
34
34
  const Component = memo<Props>((props) => {
35
35
  const memoized = useMemo<number>(() => props.value, [props.value]);
36
+
36
37
  const typed: number = useMemo(() => props.value, [props.value]);
38
+
37
39
  return <div />;
38
40
  });
39
41
  `,
@@ -54,6 +56,40 @@ describe('use-memo-hooks-style', () => {
54
56
  const value = useMemo(() => 42) as number;
55
57
  `,
56
58
  },
59
+ {
60
+ name: 'Allow satisfies expression',
61
+ code: dedent`
62
+ import { memo, useMemo } from 'react';
63
+
64
+ type Props = Readonly<{
65
+ value: number;
66
+ }>;
67
+
68
+ const Component = memo<Props>((props) => {
69
+ const value = useMemo(() => props.value, [props.value]) satisfies number;
70
+
71
+ const inner = useMemo(() => props.value satisfies number, [props.value]);
72
+
73
+ return inner + value;
74
+ });
75
+ `,
76
+ },
77
+ {
78
+ name: 'Allow const assertion',
79
+ code: dedent`
80
+ import { memo, useMemo } from 'react';
81
+
82
+ type Props = Readonly<{
83
+ value: number;
84
+ }>;
85
+
86
+ const Component = memo<Props>((props) => {
87
+ const value = useMemo(() => ({ v: props.value }) as const, [props.value]);
88
+
89
+ return value;
90
+ });
91
+ `,
92
+ },
57
93
  ],
58
94
  invalid: [
59
95
  {
@@ -62,7 +98,7 @@ describe('use-memo-hooks-style', () => {
62
98
  import { memo, useMemo } from 'react';
63
99
 
64
100
  type Props = Readonly<{
65
- readonly value: number;
101
+ value: number;
66
102
  }>;
67
103
 
68
104
  const Component = memo<Props>((props) => {
@@ -82,7 +118,7 @@ describe('use-memo-hooks-style', () => {
82
118
  import { memo, useMemo } from 'react';
83
119
 
84
120
  type Props = Readonly<{
85
- readonly value: number;
121
+ value: number;
86
122
  }>;
87
123
 
88
124
  const Component = memo<Props>((props) => {
@@ -102,11 +138,11 @@ describe('use-memo-hooks-style', () => {
102
138
  import { memo, useMemo } from 'react';
103
139
 
104
140
  type Props = Readonly<{
105
- readonly value: number;
141
+ value: number;
106
142
  }>;
107
143
 
108
144
  const Component = memo<Props>((props) => {
109
- const value = useMemo(() => props.value as number, [props.value]) ;
145
+ const value = useMemo(() => props.value as number, [props.value]);
110
146
  return value;
111
147
  });
112
148
  `,
@@ -117,16 +153,16 @@ describe('use-memo-hooks-style', () => {
117
153
  ],
118
154
  },
119
155
  {
120
- name: 'Disallow satisfies expression',
156
+ name: 'Disallow return type annotation even with satisfies',
121
157
  code: dedent`
122
158
  import { memo, useMemo } from 'react';
123
159
 
124
160
  type Props = Readonly<{
125
- readonly value: number;
161
+ value: number;
126
162
  }>;
127
163
 
128
164
  const Component = memo<Props>((props) => {
129
- const value = useMemo(() => props.value, [props.value]) satisfies number;
165
+ const value = useMemo((): number => props.value, [props.value]) satisfies number;
130
166
  return value;
131
167
  });
132
168
  `,
@@ -137,16 +173,16 @@ describe('use-memo-hooks-style', () => {
137
173
  ],
138
174
  },
139
175
  {
140
- name: 'Disallow satisfies expression (inner)',
176
+ name: 'Disallow return type annotation even with const assertion',
141
177
  code: dedent`
142
178
  import { memo, useMemo } from 'react';
143
179
 
144
180
  type Props = Readonly<{
145
- readonly value: number;
181
+ value: number;
146
182
  }>;
147
183
 
148
184
  const Component = memo<Props>((props) => {
149
- const value = useMemo(() => props.value satisfies number, [props.value]);
185
+ const value = useMemo((): number => props.value, [props.value]) as const;
150
186
  return value;
151
187
  });
152
188
  `,