diff --git a/Cargo.lock b/Cargo.lock index d87de6dd98a5..fbfc7ebe8a23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,6 +396,7 @@ dependencies = [ "countme", "insta", "lazy_static", + "log", "natord", "roaring", "rustc-hash", diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 044fa7344f5d..3d722a05940a 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -111,6 +111,7 @@ define_categories! { "lint/nursery/useGroupedTypeImport": "https://biomejs.dev/linter/rules/use-grouped-type-import", "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions", "lint/nursery/useShorthandAssign": "https://biomejs.dev/lint/rules/use-shorthand-assign", + "lint/nursery/useShorthandFunctionType": "https://biomejs.dev/lint/rules/use-shorthand-function-type", "lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread", "lint/performance/noDelete": "https://biomejs.dev/linter/rules/no-delete", "lint/security/noDangerouslySetInnerHtml": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html", diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml index 3e5a88f19547..952e2def7f2e 100644 --- a/crates/biome_js_analyze/Cargo.toml +++ b/crates/biome_js_analyze/Cargo.toml @@ -26,6 +26,7 @@ biome_json_syntax = { workspace = true } biome_rowan = { workspace = true } bpaf.workspace = true lazy_static = { workspace = true } +log = "0.4.20" natord = "1.0.9" roaring = "0.10.1" rustc-hash = { workspace = true } diff --git a/crates/biome_js_analyze/src/analyzers/nursery.rs b/crates/biome_js_analyze/src/analyzers/nursery.rs index 4890cc39b436..698798bea743 100644 --- a/crates/biome_js_analyze/src/analyzers/nursery.rs +++ b/crates/biome_js_analyze/src/analyzers/nursery.rs @@ -14,6 +14,7 @@ pub(crate) mod use_as_const_assertion; pub(crate) mod use_grouped_type_import; pub(crate) mod use_import_restrictions; pub(crate) mod use_shorthand_assign; +pub(crate) mod use_shorthand_function_type; declare_group! { pub (crate) Nursery { @@ -31,6 +32,7 @@ declare_group! { self :: use_grouped_type_import :: UseGroupedTypeImport , self :: use_import_restrictions :: UseImportRestrictions , self :: use_shorthand_assign :: UseShorthandAssign , + self :: use_shorthand_function_type :: UseShorthandFunctionType , ] } } diff --git a/crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs b/crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs new file mode 100644 index 000000000000..3d5ca548fd57 --- /dev/null +++ b/crates/biome_js_analyze/src/analyzers/nursery/use_shorthand_function_type.rs @@ -0,0 +1,193 @@ +use crate::JsRuleAction; +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, +}; +use biome_console::markup; +use biome_diagnostics::Applicability; +use biome_js_factory::make; +use biome_js_factory::make::ts_type_alias_declaration; +use biome_js_syntax::AnyTsType::TsThisType; +use biome_js_syntax::{ + AnyJsDeclarationClause, AnyTsReturnType, AnyTsType, TsCallSignatureTypeMember, TsFunctionType, + TsInterfaceDeclaration, TsObjectType, TsTypeMemberList, T, +}; +use biome_rowan::{AstNode, AstNodeList, BatchMutationExt, TriviaPieceKind}; + +declare_rule! { + /// Enforce using function types instead of object type with call signatures. + /// + /// TypeScript allows for two common ways to declare a type for a function: + /// + /// - Function type: `() => string` + /// - Object type with a signature: `{ (): string }` + /// + /// The function type form is generally preferred when possible for being more succinct. + /// + /// This rule suggests using a function type instead of an interface or object type literal with a single call signature. + /// + /// Source: https://typescript-eslint.io/rules/prefer-function-type/ + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```ts,expect_diagnostic + /// interface Example { + /// (): string; + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// function foo(example: { (): number }): number { + /// return example(); + /// } + /// ``` + /// + /// ## Valid + /// + /// ```ts + /// type Example = () => string; + /// ``` + /// + /// ```ts + /// function foo(example: () => number): number { + /// return bar(); + /// } + /// ``` + /// + /// ```ts + /// // returns the function itself, not the `this` argument. + /// type ReturnsSelf2 = (arg: string) => ReturnsSelf; + /// ``` + /// + /// ```ts + /// interface Foo { + /// bar: string; + /// } + /// interface Bar extends Foo { + /// (): void; + /// } + /// ``` + /// + /// ```ts + /// // multiple call signatures (overloads) is allowed: + /// interface Overloaded { + /// (data: string): number; + /// (id: number): string; + /// } + /// // this is equivelent to Overloaded interface. + /// type Intersection = ((data: string) => number) & ((id: number) => string); + ///``` + /// + pub(crate) UseShorthandFunctionType { + version: "1.3.0", + name: "useShorthandFunctionType", + recommended: false, + fix_kind: FixKind::Safe, + } +} + +impl Rule for UseShorthandFunctionType { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let query = ctx.query(); + + if let Some(ts_type_member_list) = query.parent::() { + // If there is more than one member, it's not a single call signature. + if ts_type_member_list.len() > 1 { + return None; + } + // If the parent is an interface with an extends clause, it's not a single call signature. + if let Some(interface_decl) = ts_type_member_list.parent::() { + if interface_decl.extends_clause().is_some() { + return None; + } + + if let AnyTsReturnType::AnyTsType(TsThisType(_)) = + query.return_type_annotation()?.ty().ok()? + { + return None; + } + } + return Some(()); + } + + None + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + Some(RuleDiagnostic::new(rule_category!(), ctx.query().range(), markup! { + "Use a function type instead of a call signature." + }).note(markup! { "Types containing only a call signature can be shortened to a function type." })) + } + + fn action(ctx: &RuleContext, _: &Self::State) -> Option { + let node = ctx.query(); + let mut mutation = ctx.root().begin(); + + let ts_type_member_list = node.parent::()?; + + if let Some(interface_decl) = ts_type_member_list.parent::() { + let type_alias_declaration = ts_type_alias_declaration( + make::token(T![type]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + interface_decl.id().ok()?, + make::token(T![=]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + AnyTsType::from(convert_ts_call_signature_type_member_to_function_type( + node, + )?), + ) + .build(); + + mutation.replace_node( + AnyJsDeclarationClause::from(interface_decl), + AnyJsDeclarationClause::from(type_alias_declaration), + ); + + return Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::Always, + message: markup! { "Convert empty interface to type alias." }.to_owned(), + mutation, + }); + } + + if let Some(ts_object_type) = ts_type_member_list.parent::() { + let new_function_type = convert_ts_call_signature_type_member_to_function_type(node)?; + + mutation.replace_node( + AnyTsType::from(ts_object_type), + AnyTsType::from(new_function_type), + ); + + return Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::Always, + message: markup! { "Convert object type to type alias." }.to_owned(), + mutation, + }); + } + + None + } +} + +fn convert_ts_call_signature_type_member_to_function_type( + node: &TsCallSignatureTypeMember, +) -> Option { + let new_node = make::ts_function_type( + make::js_parameters( + make::token(T!['(']), + node.parameters().ok()?.items(), + make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + ), + make::token(T![=>]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), + node.return_type_annotation()?.ty().ok()?, + ) + .build(); + + Some(new_node.with_type_parameters(node.type_parameters())) +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts new file mode 100644 index 000000000000..de0e7918af17 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts @@ -0,0 +1,7 @@ +interface Example { + (): string; +} + +function foo(example: { (): number }): number { + return example(); +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts.snap new file mode 100644 index 000000000000..2104c9ec3539 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/invalid.ts.snap @@ -0,0 +1,68 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.ts +--- +# Input +```js +interface Example { + (): string; +} + +function foo(example: { (): number }): number { + return example(); +} +``` + +# Diagnostics +``` +invalid.ts:2:2 lint/nursery/useShorthandFunctionType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use a function type instead of a call signature. + + 1 │ interface Example { + > 2 │ (): string; + │ ^^^^^^^^^^^ + 3 │ } + 4 │ + + i Types containing only a call signature can be shortened to a function type. + + i Safe fix: Convert empty interface to type alias. + + 1 │ - interface·Example·{ + 2 │ - ·():·string; + 3 │ - } + 1 │ + type·Example·=·()·=>·string + 4 2 │ + 5 3 │ function foo(example: { (): number }): number { + + +``` + +``` +invalid.ts:5:25 lint/nursery/useShorthandFunctionType FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use a function type instead of a call signature. + + 3 │ } + 4 │ + > 5 │ function foo(example: { (): number }): number { + │ ^^^^^^^^^^ + 6 │ return example(); + 7 │ } + + i Types containing only a call signature can be shortened to a function type. + + i Safe fix: Convert object type to type alias. + + 3 3 │ } + 4 4 │ + 5 │ - function·foo(example:·{·():·number·}):·number·{ + 5 │ + function·foo(example:·()·=>·number):·number·{ + 6 6 │ return example(); + 7 7 │ } + + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/valid.ts new file mode 100644 index 000000000000..bae591fde495 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/valid.ts @@ -0,0 +1,30 @@ +type Example = () => string; + +function foo(example: () => number): number { + return bar(); +} + +// returns the function itself, not the `this` argument. +type ReturnsSelf = (arg: string) => ReturnsSelf; + +interface Foo { + bar: string; +} + +interface Bar extends Foo { + (): void; +} + +// multiple call signatures (overloads) is allowed: +interface Overloaded { + (data: string): number; + (id: number): string; +} + +// this is equivelent to Overloaded interface. +type Intersection = ((data: string) => number) & ((id: number) => string); + +interface ReturnsSelf { + // returns the function itself, not the `this` argument. + (arg: string): this; +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/valid.ts.snap new file mode 100644 index 000000000000..56eb2704baf5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useShorthandFunctionType/valid.ts.snap @@ -0,0 +1,39 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.ts +--- +# Input +```js +type Example = () => string; + +function foo(example: () => number): number { + return bar(); +} + +// returns the function itself, not the `this` argument. +type ReturnsSelf = (arg: string) => ReturnsSelf; + +interface Foo { + bar: string; +} + +interface Bar extends Foo { + (): void; +} + +// multiple call signatures (overloads) is allowed: +interface Overloaded { + (data: string): number; + (id: number): string; +} + +// this is equivelent to Overloaded interface. +type Intersection = ((data: string) => number) & ((id: number) => string); + +interface ReturnsSelf { + // returns the function itself, not the `this` argument. + (arg: string): this; +} +``` + + diff --git a/crates/biome_service/src/configuration/linter/rules.rs b/crates/biome_service/src/configuration/linter/rules.rs index 54003e25c696..b5b7b8a2c535 100644 --- a/crates/biome_service/src/configuration/linter/rules.rs +++ b/crates/biome_service/src/configuration/linter/rules.rs @@ -2722,6 +2722,15 @@ pub struct Nursery { #[bpaf(long("use-shorthand-assign"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub use_shorthand_assign: Option, + #[doc = "Enforce using function types instead of object type with call signatures."] + #[bpaf( + long("use-shorthand-function-type"), + argument("on|off|warn"), + optional, + hide + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_shorthand_function_type: Option, } impl MergeWith for Nursery { fn merge_with(&mut self, other: Nursery) { @@ -2785,6 +2794,9 @@ impl MergeWith for Nursery { if let Some(use_shorthand_assign) = other.use_shorthand_assign { self.use_shorthand_assign = Some(use_shorthand_assign); } + if let Some(use_shorthand_function_type) = other.use_shorthand_function_type { + self.use_shorthand_function_type = Some(use_shorthand_function_type); + } } fn merge_with_if_not_default(&mut self, other: Nursery) where @@ -2797,7 +2809,7 @@ impl MergeWith for Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 18] = [ + pub(crate) const GROUP_RULES: [&'static str; 19] = [ "noApproximativeNumericConstant", "noDuplicateJsonKeys", "noEmptyBlockStatements", @@ -2816,6 +2828,7 @@ impl Nursery { "useGroupedTypeImport", "useImportRestrictions", "useShorthandAssign", + "useShorthandFunctionType", ]; const RECOMMENDED_RULES: [&'static str; 8] = [ "noDuplicateJsonKeys", @@ -2837,7 +2850,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 18] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 19] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2856,6 +2869,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { @@ -2962,6 +2976,11 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } + if let Some(rule) = self.use_shorthand_function_type.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3056,6 +3075,11 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } + if let Some(rule) = self.use_shorthand_function_type.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3069,7 +3093,7 @@ impl Nursery { pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 8] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 18] { + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 19] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] @@ -3114,6 +3138,7 @@ impl Nursery { "useGroupedTypeImport" => self.use_grouped_type_import.as_ref(), "useImportRestrictions" => self.use_import_restrictions.as_ref(), "useShorthandAssign" => self.use_shorthand_assign.as_ref(), + "useShorthandFunctionType" => self.use_shorthand_function_type.as_ref(), _ => None, } } diff --git a/crates/biome_service/src/configuration/parse/json/rules.rs b/crates/biome_service/src/configuration/parse/json/rules.rs index e742233cfb19..a5b12923a20c 100644 --- a/crates/biome_service/src/configuration/parse/json/rules.rs +++ b/crates/biome_service/src/configuration/parse/json/rules.rs @@ -904,6 +904,15 @@ impl VisitNode for Nursery { configuration.map_rule_configuration(&value, "useShorthandAssign", diagnostics)?; self.use_shorthand_assign = Some(configuration); } + "useShorthandFunctionType" => { + let mut configuration = RuleConfiguration::default(); + configuration.map_rule_configuration( + &value, + "useShorthandFunctionType", + diagnostics, + )?; + self.use_shorthand_function_type = Some(configuration); + } _ => { report_unknown_map_key( &name, @@ -928,6 +937,7 @@ impl VisitNode for Nursery { "useGroupedTypeImport", "useImportRestrictions", "useShorthandAssign", + "useShorthandFunctionType", ], diagnostics, ); diff --git a/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap b/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap index 579f2259cfa6..8429207c673d 100644 --- a/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap +++ b/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap @@ -35,6 +35,7 @@ hooks_incorrect_options.json:6:5 deserialize ━━━━━━━━━━━ - useGroupedTypeImport - useImportRestrictions - useShorthandAssign + - useShorthandFunctionType diff --git a/crates/biome_service/tests/invalid/hooks_missing_name.json.snap b/crates/biome_service/tests/invalid/hooks_missing_name.json.snap index b67aeb71517a..a0ce046657af 100644 --- a/crates/biome_service/tests/invalid/hooks_missing_name.json.snap +++ b/crates/biome_service/tests/invalid/hooks_missing_name.json.snap @@ -35,6 +35,7 @@ hooks_missing_name.json:6:5 deserialize ━━━━━━━━━━━━━ - useGroupedTypeImport - useImportRestrictions - useShorthandAssign + - useShorthandFunctionType diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 84b0a407e17f..6c31c5caa7f0 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -1159,6 +1159,13 @@ { "$ref": "#/definitions/RuleConfiguration" }, { "type": "null" } ] + }, + "useShorthandFunctionType": { + "description": "Enforce using function types instead of object type with call signatures.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] } } }, diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 413e878d27b1..9e9d463cec72 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -808,6 +808,10 @@ export interface Nursery { * Require assignment operator shorthand where possible. */ useShorthandAssign?: RuleConfiguration; + /** + * Enforce using function types instead of object type with call signatures. + */ + useShorthandFunctionType?: RuleConfiguration; } /** * A list of rules that belong to this group @@ -1451,6 +1455,7 @@ export type Category = | "lint/nursery/useGroupedTypeImport" | "lint/nursery/useImportRestrictions" | "lint/nursery/useShorthandAssign" + | "lint/nursery/useShorthandFunctionType" | "lint/performance/noAccumulatingSpread" | "lint/performance/noDelete" | "lint/security/noDangerouslySetInnerHtml" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 84b0a407e17f..6c31c5caa7f0 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1159,6 +1159,13 @@ { "$ref": "#/definitions/RuleConfiguration" }, { "type": "null" } ] + }, + "useShorthandFunctionType": { + "description": "Enforce using function types instead of object type with call signatures.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] } } }, diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index ddca54d93573..24b833c9c0eb 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 171 rules

\ No newline at end of file +

Biome's linter has a total of 172 rules

\ No newline at end of file diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index 08c2e69d2b45..ff18fd6ec458 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -233,3 +233,4 @@ Rules that belong to this group are not subject to semantic versionimport type when an import only has specifiers with type qualifier. | ⚠️ | | [useImportRestrictions](/linter/rules/use-import-restrictions) | Disallows package private imports. | | | [useShorthandAssign](/linter/rules/use-shorthand-assign) | Require assignment operator shorthand where possible. | ⚠️ | +| [useShorthandFunctionType](/linter/rules/use-shorthand-function-type) | Enforce using function types instead of object type with call signatures. | 🔧 | diff --git a/website/src/content/docs/linter/rules/use-shorthand-function-type.md b/website/src/content/docs/linter/rules/use-shorthand-function-type.md new file mode 100644 index 000000000000..9a2bacc15deb --- /dev/null +++ b/website/src/content/docs/linter/rules/use-shorthand-function-type.md @@ -0,0 +1,121 @@ +--- +title: useShorthandFunctionType (since v1.3.0) +--- + +**Diagnostic Category: `lint/nursery/useShorthandFunctionType`** + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Enforce using function types instead of object type with call signatures. + +TypeScript allows for two common ways to declare a type for a function: + +- Function type: `() => string` +- Object type with a signature: `{ (): string }` + +The function type form is generally preferred when possible for being more succinct. + +This rule suggests using a function type instead of an interface or object type literal with a single call signature. + +Source: https://typescript-eslint.io/rules/prefer-function-type/ + +## Examples + +### Invalid + +```ts +interface Example { + (): string; +} +``` + +

nursery/useShorthandFunctionType.js:2:3 lint/nursery/useShorthandFunctionType  FIXABLE  ━━━━━━━━━━━━
+
+   Use a function type instead of a call signature.
+  
+    1 │ interface Example {
+  > 2 │   (): string;
+     ^^^^^^^^^^^
+    3 │ }
+    4 │ 
+  
+   Types containing only a call signature can be shortened to a function type.
+  
+   Safe fix: Convert empty interface to type alias.
+  
+    1  - interface·Example·{
+    2  - ··():·string;
+    3  - }
+      1+ type·Example·=·()·=>·string
+    4 2  
+  
+
+ +```ts +function foo(example: { (): number }): number { + return example(); +} +``` + +
nursery/useShorthandFunctionType.js:1:25 lint/nursery/useShorthandFunctionType  FIXABLE  ━━━━━━━━━━━
+
+   Use a function type instead of a call signature.
+  
+  > 1 │ function foo(example: { (): number }): number {
+                           ^^^^^^^^^^
+    2 │   return example();
+    3 │ }
+  
+   Types containing only a call signature can be shortened to a function type.
+  
+   Safe fix: Convert object type to type alias.
+  
+    1  - function·foo(example:·{·():·number·}):·number·{
+      1+ function·foo(example:·()·=>·number):·number·{
+    2 2    return example();
+    3 3  }
+  
+
+ +## Valid + +```ts +type Example = () => string; +``` + +```ts +function foo(example: () => number): number { + return bar(); +} +``` + +```ts +// returns the function itself, not the `this` argument. +type ReturnsSelf2 = (arg: string) => ReturnsSelf; +``` + +```ts +interface Foo { + bar: string; +} +interface Bar extends Foo { + (): void; +} +``` + +```ts +// multiple call signatures (overloads) is allowed: +interface Overloaded { + (data: string): number; + (id: number): string; +} +// this is equivelent to Overloaded interface. +type Intersection = ((data: string) => number) & ((id: number) => string); +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)