Skip to content

Commit b243830

Browse files
committed
feat: add component-derived TypeScript Props names
1 parent c3d6960 commit b243830

8 files changed

Lines changed: 104 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ Accepts a comma-separated list of VS Code language identifiers.
101101

102102
Controls whether Props and State declarations use type aliases or interfaces in TypeScript snippets.
103103

104+
### `typescriptPropsNaming`
105+
106+
**Type:** `string`
107+
108+
**Default:** `generic`
109+
110+
**Allowed values:** `generic`, `component`
111+
112+
Controls whether TypeScript component snippets name their props type `Props` or derive it from the component name, for example `ButtonProps`.
113+
104114
### `componentWrapper`
105115

106116
**Type:** `string`

‎docs/Snippets.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ const mapDispatchToProps = {};
479479

480480
## TypeScript Components
481481

482-
All TypeScript component snippets use `type` for Props/State by default. Change to `interface` via the `typescriptPropsStatePrefix` setting.
482+
All TypeScript component snippets use `type` for Props/State by default. Change to `interface` via the `typescriptPropsStatePrefix` setting. Set `typescriptPropsNaming` to `component` to derive the Props type from the component name, for example `ButtonProps`.
483483

484484
| Prefix | Method |
485485
| -----------: | ------------------------------------------------------- |

‎package.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@
122122
],
123123
"scope": "application"
124124
},
125+
"reactSnippets.settings.typescriptPropsNaming": {
126+
"type": "string",
127+
"markdownDescription": "Controls whether TypeScript component snippets name their props type `Props` or derive it from the component name, for example `ButtonProps`.",
128+
"default": "generic",
129+
"enum": [
130+
"generic",
131+
"component"
132+
],
133+
"scope": "application"
134+
},
125135
"reactSnippets.settings.componentWrapper": {
126136
"type": "string",
127137
"markdownDescription": "Controls whether component snippets wrap their content in a React fragment or a `div`.",

‎src/helpers/extensionConfig.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ComponentWrapper,
55
DEFAULT_GENERATION_SETTINGS,
66
GenerationSettings,
7+
TypescriptPropsNaming,
78
} from '../types';
89

910
export type ExtensionSettings = GenerationSettings;
@@ -28,6 +29,10 @@ const extensionConfig = (): ExtensionSettings => {
2829
'typescriptPropsStatePrefix',
2930
DEFAULT_GENERATION_SETTINGS.typescriptPropsStatePrefix,
3031
),
32+
typescriptPropsNaming: config.get<TypescriptPropsNaming>(
33+
'typescriptPropsNaming',
34+
DEFAULT_GENERATION_SETTINGS.typescriptPropsNaming,
35+
),
3136
componentWrapper: config.get<ComponentWrapper>(
3237
'componentWrapper',
3338
DEFAULT_GENERATION_SETTINGS.componentWrapper,

‎src/helpers/snippetPlaceholders.ts‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1-
import {
2-
GenerationSettings,
3-
Mappings,
4-
Placeholders,
5-
} from '../types';
1+
import { GenerationSettings, Mappings, Placeholders } from '../types';
2+
3+
const propsPlaceholders = {
4+
generic: {
5+
type: Mappings.TypeProps,
6+
interface: Mappings.InterfaceProps,
7+
},
8+
component: {
9+
type: Mappings.ComponentTypeProps,
10+
interface: Mappings.ComponentInterfaceProps,
11+
},
12+
} as const;
613

714
export const replaceSnippetPlaceholders = (
815
snippetString: string,
916
settings: GenerationSettings,
1017
): string => {
11-
const { componentWrapper, typescriptPropsStatePrefix } = settings;
18+
const {
19+
componentWrapper,
20+
typescriptPropsNaming,
21+
typescriptPropsStatePrefix,
22+
} = settings;
1223
const propsPlaceholder =
13-
typescriptPropsStatePrefix === 'type'
14-
? Mappings.TypeProps
15-
: Mappings.InterfaceProps;
24+
propsPlaceholders[typescriptPropsNaming][typescriptPropsStatePrefix];
25+
const propsName =
26+
typescriptPropsNaming === 'component'
27+
? Mappings.ComponentProps
28+
: Mappings.Props;
1629
const statePlaceholder =
1730
typescriptPropsStatePrefix === 'type'
1831
? Mappings.TypeState
@@ -23,13 +36,14 @@ export const replaceSnippetPlaceholders = (
2336
: Mappings.DivComponentWrapper;
2437

2538
return String(snippetString)
39+
.replaceAll(Placeholders.TypeProps, propsPlaceholder)
40+
.replaceAll(Placeholders.TypeState, statePlaceholder)
41+
.replaceAll(Placeholders.ComponentProps, propsName)
2642
.replaceAll(Placeholders.FileName, Mappings.FileName)
2743
.replaceAll(Placeholders.ComponentWrapper, componentWrapperPlaceholder)
2844
.replaceAll(Placeholders.FirstTab, Mappings.FirstTab)
2945
.replaceAll(Placeholders.SecondTab, Mappings.SecondTab)
3046
.replaceAll(Placeholders.ThirdTab, Mappings.ThirdTab)
3147
.replaceAll(Placeholders.Capitalize, Mappings.Capitalize)
32-
.replaceAll(Placeholders.TypeProps, propsPlaceholder)
33-
.replaceAll(Placeholders.TypeState, statePlaceholder)
3448
.replaceAll(Placeholders.FourthTabCapitalize, Mappings.FourthTabCapitalize);
3549
};

‎src/sourceSnippets/typescript.ts‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const typescriptReactClassComponent: TypescriptSnippet = {
5454
...reactComponent,
5555
'',
5656
...propsStateInterface,
57-
`export default class ${Placeholders.FileName} extends Component<Props, State> {`,
57+
`export default class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`,
5858
' state = {}',
5959
'',
6060
...innerComponentReturn,
@@ -69,7 +69,7 @@ const typescriptReactClassExportComponent: TypescriptSnippet = {
6969
...reactComponent,
7070
'',
7171
...propsStateInterface,
72-
`class ${Placeholders.FileName} extends Component<Props, State> {`,
72+
`class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`,
7373
' state = {}',
7474
'',
7575
...innerComponentReturn,
@@ -87,7 +87,7 @@ const typescriptReactFunctionalExportComponent: TypescriptSnippet = {
8787
...react,
8888
'',
8989
...propsTypeInterface,
90-
`function ${Placeholders.FileName}({}: Props) {`,
90+
`function ${Placeholders.FileName}({}: ${Placeholders.ComponentProps}) {`,
9191
...innerComponent,
9292
'}',
9393
...exportDefault,
@@ -103,7 +103,7 @@ const typescriptReactFunctionalComponent: TypescriptSnippet = {
103103
...react,
104104
'',
105105
...propsTypeInterface,
106-
`export default function ${Placeholders.FileName}({}: Props) {`,
106+
`export default function ${Placeholders.FileName}({}: ${Placeholders.ComponentProps}) {`,
107107
...innerComponent,
108108
'}',
109109
],
@@ -118,7 +118,7 @@ const typescriptReactArrowFunctionExportComponent: TypescriptSnippet = {
118118
...react,
119119
'',
120120
...propsTypeInterface,
121-
`const ${Placeholders.FileName} = (props: Props) => {`,
121+
`const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`,
122122
...innerComponent,
123123
'}',
124124
...exportDefault,
@@ -134,7 +134,7 @@ const typescriptReactArrowFunctionComponent: TypescriptSnippet = {
134134
...react,
135135
'',
136136
...propsTypeInterface,
137-
`export const ${Placeholders.FileName} = (props: Props) => {`,
137+
`export const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`,
138138
...innerComponent,
139139
'}',
140140
],
@@ -149,7 +149,7 @@ const typescriptReactClassPureComponent: TypescriptSnippet = {
149149
...reactPureComponent,
150150
'',
151151
...propsTypeInterface,
152-
`export default class ${Placeholders.FileName} extends PureComponent<Props> {`,
152+
`export default class ${Placeholders.FileName} extends PureComponent<${Placeholders.ComponentProps}> {`,
153153
...innerComponentReturn,
154154
'}',
155155
],
@@ -164,7 +164,7 @@ const typescriptReactClassExportPureComponent: TypescriptSnippet = {
164164
...reactPureComponent,
165165
'',
166166
...propsTypeInterface,
167-
`class ${Placeholders.FileName} extends PureComponent<Props> {`,
167+
`class ${Placeholders.FileName} extends PureComponent<${Placeholders.ComponentProps}> {`,
168168
...innerComponentReturn,
169169
'}',
170170
...exportDefault,
@@ -181,7 +181,7 @@ const typescriptReactClassComponentRedux: TypescriptSnippet = {
181181
...reactComponent,
182182
'',
183183
...propsStateInterface,
184-
`export class ${Placeholders.FileName} extends Component<Props, State> {`,
184+
`export class ${Placeholders.FileName} extends Component<${Placeholders.ComponentProps}, State> {`,
185185
' state = {}',
186186
'',
187187
...innerComponentReturn,
@@ -200,7 +200,7 @@ const typescriptReactNativeArrowFunctionComponent: TypescriptSnippet = {
200200
...react,
201201
'',
202202
...propsTypeInterface,
203-
`const ${Placeholders.FileName} = (props: Props) => {`,
203+
`const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`,
204204
' return (',
205205
' <View>',
206206
` <Text>${Placeholders.FirstTab}</Text>`,
@@ -222,7 +222,7 @@ const typescriptReactNativeArrowFunctionComponentWithStyles: TypescriptSnippet =
222222
...react,
223223
'',
224224
...propsTypeInterface,
225-
`const ${Placeholders.FileName} = (props: Props) => {`,
225+
`const ${Placeholders.FileName} = (props: ${Placeholders.ComponentProps}) => {`,
226226
' return (',
227227
' <View>',
228228
` <Text>${Placeholders.FirstTab}</Text>`,

‎src/types.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ export const VALID_LANGUAGE_SCOPES = [
1616
export type LanguageScope = (typeof VALID_LANGUAGE_SCOPES)[number];
1717

1818
export type ComponentWrapper = 'fragment' | 'div';
19+
export type TypescriptPropsNaming = 'generic' | 'component';
1920

2021
export type GenerationSettings = {
2122
readonly languageScopes: string;
2223
readonly importReactOnTop: boolean;
2324
readonly typescript: boolean;
2425
readonly typescriptPropsStatePrefix: 'type' | 'interface';
26+
readonly typescriptPropsNaming: TypescriptPropsNaming;
2527
readonly componentWrapper: ComponentWrapper;
2628
};
2729

@@ -30,6 +32,7 @@ export const DEFAULT_GENERATION_SETTINGS: GenerationSettings = {
3032
importReactOnTop: false,
3133
typescript: true,
3234
typescriptPropsStatePrefix: 'type',
35+
typescriptPropsNaming: 'generic',
3336
componentWrapper: 'fragment',
3437
};
3538

@@ -41,6 +44,7 @@ export const Placeholders = {
4144
Capitalize: 'capitalize',
4245
TypeProps: 'typeProps',
4346
TypeState: 'typeState',
47+
ComponentProps: 'componentProps',
4448
ComponentWrapper: 'componentWrapper',
4549
FourthTabCapitalize: 'fourthTabCapitalize',
4650
} as const;
@@ -52,9 +56,13 @@ export const Mappings = {
5256
ThirdTab: '${4:third}',
5357
Capitalize: '${2/(.*)/${1:/capitalize}/}',
5458
TypeProps: 'type Props = {}',
59+
ComponentTypeProps: 'type fileProps = {}',
5560
TypeState: 'type State = {}',
5661
InterfaceProps: 'interface Props {}',
62+
ComponentInterfaceProps: 'interface fileProps {}',
5763
InterfaceState: 'interface State {}',
64+
Props: 'Props',
65+
ComponentProps: 'fileProps',
5866
FragmentComponentWrapper: '<>first</>',
5967
DivComponentWrapper: '<div>first</div>',
6068
FourthTabCapitalize: '${4/(.*)/${1:/capitalize}/}',

‎tests/generation.test.ts‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const parseManifestGenerationDefaults = (
7676
const importReactOnTop = readDefault('importReactOnTop');
7777
const typescript = readDefault('typescript');
7878
const typescriptPropsStatePrefix = readDefault('typescriptPropsStatePrefix');
79+
const typescriptPropsNaming = readDefault('typescriptPropsNaming');
7980
const componentWrapper = readDefault('componentWrapper');
8081

8182
if (
@@ -84,6 +85,8 @@ const parseManifestGenerationDefaults = (
8485
typeof typescript !== 'boolean' ||
8586
(typescriptPropsStatePrefix !== 'type' &&
8687
typescriptPropsStatePrefix !== 'interface') ||
88+
(typescriptPropsNaming !== 'generic' &&
89+
typescriptPropsNaming !== 'component') ||
8790
(componentWrapper !== 'fragment' && componentWrapper !== 'div')
8891
) {
8992
throw new Error('Package generation defaults have invalid types');
@@ -94,6 +97,7 @@ const parseManifestGenerationDefaults = (
9497
importReactOnTop,
9598
typescript,
9699
typescriptPropsStatePrefix,
100+
typescriptPropsNaming,
97101
componentWrapper,
98102
};
99103
};
@@ -260,6 +264,19 @@ describe('snippet compiler', () => {
260264
componentWrapper: 'div',
261265
}),
262266
);
267+
const componentNamedProps = parseGeneratedSnippets(
268+
buildSnippets({
269+
...DEFAULT_GENERATION_SETTINGS,
270+
typescriptPropsNaming: 'component',
271+
}),
272+
);
273+
const componentNamedInterfaces = parseGeneratedSnippets(
274+
buildSnippets({
275+
...DEFAULT_GENERATION_SETTINGS,
276+
typescriptPropsNaming: 'component',
277+
typescriptPropsStatePrefix: 'interface',
278+
}),
279+
);
263280

264281
expect(legacyReact.typescriptReactClassComponent).toBeUndefined();
265282
expect(
@@ -277,12 +294,26 @@ describe('snippet compiler', () => {
277294
expect(
278295
requireSnippet(divWrappedComponents, 'reactFunctionalComponent').body,
279296
).toContain(' <div>${2:first}</div>');
297+
expect(
298+
requireSnippet(divWrappedComponents, 'typescriptReactClassComponent')
299+
.body,
300+
).toContain(' <div>${2:first}</div>');
301+
expect(
302+
requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent')
303+
.body,
304+
).toContain('type ${1:${TM_FILENAME_BASE}}Props = {}');
305+
expect(
306+
requireSnippet(componentNamedProps, 'typescriptReactFunctionalComponent')
307+
.body,
308+
).toContain(
309+
'export default function ${1:${TM_FILENAME_BASE}}({}: ${1:${TM_FILENAME_BASE}}Props) {',
310+
);
280311
expect(
281312
requireSnippet(
282-
divWrappedComponents,
283-
'typescriptReactClassComponent',
313+
componentNamedInterfaces,
314+
'typescriptReactFunctionalComponent',
284315
).body,
285-
).toContain(' <div>${2:first}</div>');
316+
).toContain('interface ${1:${TM_FILENAME_BASE}}Props {}');
286317
});
287318

288319
test('uses defaults for empty scopes but rejects nonempty invalid scopes', () => {

0 commit comments

Comments
 (0)