Repository: hetznercloud/cli
Pull request #972 (2025-01-31 15:59:27)

refactor: make base describe command generic
More type safety <3

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/hetznercloud/cli/pull/972?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) Report
Attention: Patch coverage is `58.82353%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 46.75%. Comparing base [(`0ccd41c`)](https://app.codecov.io/gh/hetznercloud/cli/commit/0ccd41c38442b701e87b5646da5d8a960e5e872d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) to head [(`f114b2e`)](https://app.codecov.io/gh/hetznercloud/cli/commit/f114b2e8c80cfac7d85d823a6214c2a26cbaaee7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/hetznercloud/cli/pull/972?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | Patch % | Lines |
|---|---|---|
| [internal/cmd/image/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Fimage%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL2ltYWdlL2Rlc2NyaWJlLmdv) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [internal/cmd/loadbalancer/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Floadbalancer%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL2xvYWRiYWxhbmNlci9kZXNjcmliZS5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [internal/cmd/location/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Flocation%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL2xvY2F0aW9uL2Rlc2NyaWJlLmdv) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [internal/cmd/primaryip/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Fprimaryip%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL3ByaW1hcnlpcC9kZXNjcmliZS5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [internal/cmd/server/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Fserver%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL3NlcnZlci9kZXNjcmliZS5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [internal/cmd/servertype/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Fservertype%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL3NlcnZlcnR5cGUvZGVzY3JpYmUuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [internal/cmd/volume/describe.go](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&filepath=internal%2Fcmd%2Fvolume%2Fdescribe.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aW50ZXJuYWwvY21kL3ZvbHVtZS9kZXNjcmliZS5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/cli/pull/972?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main     #972       +/-   ##
===========================================
- Coverage   70.01%   46.75%   -23.26%     
===========================================
  Files         245      239        -6     
  Lines       10715    10411      -304     
===========================================
- Hits         7502     4868     -2634     
- Misses       2537     5235     +2698     
+ Partials      676      308      -368     
```

| [Flag](https://app.codecov.io/gh/hetznercloud/cli/pull/972/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | Coverage Δ | |
|---|---|---|
| [e2e](https://app.codecov.io/gh/hetznercloud/cli/pull/972/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | `46.75% <58.82%> (-0.03%)` | :arrow_down: |
| [unit](https://app.codecov.io/gh/hetznercloud/cli/pull/972/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/hetznercloud/cli/pull/972?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).


Patch:
diff --git a/internal/cmd/base/describe.go b/internal/cmd/base/describe.go
index 73fa892..55615ff 100644
--- a/internal/cmd/base/describe.go
+++ b/internal/cmd/base/describe.go
@@ -16,7 +16,7 @@ import (
 )
 
 // DescribeCmd allows defining commands for describing a resource.
-type DescribeCmd struct {
+type DescribeCmd[T any] struct {
 	ResourceNameSingular string // e.g. "server"
 	ShortDescription     string
 	// key in API response JSON to use for extracting object from response body for JSON output.
@@ -26,12 +26,12 @@ type DescribeCmd struct {
 	AdditionalFlags  func(*cobra.Command)
 	// Fetch is called to fetch the resource to describe.
 	// The first returned interface is the resource itself as a hcloud struct, the second is the schema for the resource.
-	Fetch     func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, interface{}, error)
-	PrintText func(s state.State, cmd *cobra.Command, resource interface{}) error
+	Fetch     func(s state.State, cmd *cobra.Command, idOrName string) (T, any, error)
+	PrintText func(s state.State, cmd *cobra.Command, resource T) error
 }
 
 // CobraCommand creates a command that can be registered with cobra.
-func (dc *DescribeCmd) CobraCommand(s state.State) *cobra.Command {
+func (dc *DescribeCmd[T]) CobraCommand(s state.State) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:                   fmt.Sprintf("describe [options] <%s>", util.ToKebabCase(dc.ResourceNameSingular)),
 		Short:                 dc.ShortDescription,
@@ -52,7 +52,7 @@ func (dc *DescribeCmd) CobraCommand(s state.State) *cobra.Command {
 }
 
 // Run executes a describe command.
-func (dc *DescribeCmd) Run(s state.State, cmd *cobra.Command, args []string) error {
+func (dc *DescribeCmd[T]) Run(s state.State, cmd *cobra.Command, args []string) error {
 	outputFlags := output.FlagsForCommand(cmd)
 
 	quiet, err := config.OptionQuiet.Get(s.Config())
diff --git a/internal/cmd/certificate/describe.go b/internal/cmd/certificate/describe.go
index dc34fe9..c6bc11a 100644
--- a/internal/cmd/certificate/describe.go
+++ b/internal/cmd/certificate/describe.go
@@ -11,21 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Certificate]{
 	ResourceNameSingular: "certificate",
 	ShortDescription:     "Describe an certificate",
 	JSONKeyGetByID:       "certificate",
 	JSONKeyGetByName:     "certificates",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Certificate().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Certificate, any, error) {
 		cert, _, err := s.Client().Certificate().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return cert, hcloud.SchemaFromCertificate(cert), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		cert := resource.(*hcloud.Certificate)
+	PrintText: func(s state.State, cmd *cobra.Command, cert *hcloud.Certificate) error {
 		cmd.Printf("ID:\t\t\t%d\n", cert.ID)
 		cmd.Printf("Name:\t\t\t%s\n", cert.Name)
 		cmd.Printf("Type:\t\t\t%s\n", cert.Type)
diff --git a/internal/cmd/datacenter/describe.go b/internal/cmd/datacenter/describe.go
index 16e3e21..7786266 100644
--- a/internal/cmd/datacenter/describe.go
+++ b/internal/cmd/datacenter/describe.go
@@ -12,22 +12,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Datacenter]{
 	ResourceNameSingular: "datacenter",
 	ShortDescription:     "Describe an datacenter",
 	JSONKeyGetByID:       "datacenter",
 	JSONKeyGetByName:     "datacenters",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Datacenter().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Datacenter, any, error) {
 		dc, _, err := s.Client().Datacenter().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return dc, hcloud.SchemaFromDatacenter(dc), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		datacenter := resource.(*hcloud.Datacenter)
-
+	PrintText: func(s state.State, cmd *cobra.Command, datacenter *hcloud.Datacenter) error {
 		cmd.Printf("ID:\t\t%d\n", datacenter.ID)
 		cmd.Printf("Name:\t\t%s\n", datacenter.Name)
 		cmd.Printf("Description:\t%s\n", datacenter.Description)
diff --git a/internal/cmd/firewall/describe.go b/internal/cmd/firewall/describe.go
index 956ab35..14e56cd 100644
--- a/internal/cmd/firewall/describe.go
+++ b/internal/cmd/firewall/describe.go
@@ -13,22 +13,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Firewall]{
 	ResourceNameSingular: "firewall",
 	ShortDescription:     "Describe an firewall",
 	JSONKeyGetByID:       "firewall",
 	JSONKeyGetByName:     "firewalls",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Firewall().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Firewall, any, error) {
 		fw, _, err := s.Client().Firewall().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return fw, hcloud.SchemaFromFirewall(fw), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		firewall := resource.(*hcloud.Firewall)
-
+	PrintText: func(s state.State, cmd *cobra.Command, firewall *hcloud.Firewall) error {
 		cmd.Printf("ID:\t\t%d\n", firewall.ID)
 		cmd.Printf("Name:\t\t%s\n", firewall.Name)
 		cmd.Printf("Created:\t%s (%s)\n", util.Datetime(firewall.Created), humanize.Time(firewall.Created))
diff --git a/internal/cmd/floatingip/describe.go b/internal/cmd/floatingip/describe.go
index 4909ae5..3f63852 100644
--- a/internal/cmd/floatingip/describe.go
+++ b/internal/cmd/floatingip/describe.go
@@ -11,22 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.FloatingIP]{
 	ResourceNameSingular: "Floating IP",
 	ShortDescription:     "Describe an Floating IP",
 	JSONKeyGetByID:       "floating_ip",
 	JSONKeyGetByName:     "floating_ips",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.FloatingIP().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.FloatingIP, any, error) {
 		ip, _, err := s.Client().FloatingIP().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return ip, hcloud.SchemaFromFloatingIP(ip), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		floatingIP := resource.(*hcloud.FloatingIP)
-
+	PrintText: func(s state.State, cmd *cobra.Command, floatingIP *hcloud.FloatingIP) error {
 		cmd.Printf("ID:\t\t%d\n", floatingIP.ID)
 		cmd.Printf("Type:\t\t%s\n", floatingIP.Type)
 		cmd.Printf("Name:\t\t%s\n", floatingIP.Name)
diff --git a/internal/cmd/image/describe.go b/internal/cmd/image/describe.go
index d547858..eabe552 100644
--- a/internal/cmd/image/describe.go
+++ b/internal/cmd/image/describe.go
@@ -16,7 +16,7 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Image]{
 	ResourceNameSingular: "image",
 	ShortDescription:     "Describe an image",
 	JSONKeyGetByID:       "image",
@@ -26,7 +26,7 @@ var DescribeCmd = base.DescribeCmd{
 		_ = cmd.RegisterFlagCompletionFunc("architecture", cmpl.SuggestCandidates(string(hcloud.ArchitectureX86), string(hcloud.ArchitectureARM)))
 	},
 	NameSuggestions: func(c hcapi2.Client) func() []string { return c.Image().Names },
-	Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, cmd *cobra.Command, idOrName string) (*hcloud.Image, any, error) {
 		_, err := strconv.ParseInt(idOrName, 10, 64)
 		isID := err == nil
 
@@ -45,9 +45,7 @@ var DescribeCmd = base.DescribeCmd{
 		}
 		return img, hcloud.SchemaFromImage(img), nil
 	},
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		image := resource.(*hcloud.Image)
-
+	PrintText: func(_ state.State, cmd *cobra.Command, image *hcloud.Image) error {
 		cmd.Printf("ID:\t\t%d\n", image.ID)
 		cmd.Printf("Type:\t\t%s\n", image.Type)
 		cmd.Printf("Status:\t\t%s\n", image.Status)
diff --git a/internal/cmd/iso/describe.go b/internal/cmd/iso/describe.go
index 1eac82e..148b144 100644
--- a/internal/cmd/iso/describe.go
+++ b/internal/cmd/iso/describe.go
@@ -11,22 +11,20 @@ import (
 )
 
 // DescribeCmd defines a command for describing a iso.
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.ISO]{
 	ResourceNameSingular: "iso",
 	ShortDescription:     "Describe a iso",
 	JSONKeyGetByID:       "iso",
 	JSONKeyGetByName:     "isos",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Location().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.ISO, any, error) {
 		iso, _, err := s.Client().ISO().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return iso, hcloud.SchemaFromISO(iso), nil
 	},
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		iso := resource.(*hcloud.ISO)
-
+	PrintText: func(_ state.State, cmd *cobra.Command, iso *hcloud.ISO) error {
 		cmd.Printf("ID:\t\t%d\n", iso.ID)
 		cmd.Printf("Name:\t\t%s\n", iso.Name)
 		cmd.Printf("Description:\t%s\n", iso.Description)
diff --git a/internal/cmd/loadbalancer/describe.go b/internal/cmd/loadbalancer/describe.go
index afd2336..808f21c 100644
--- a/internal/cmd/loadbalancer/describe.go
+++ b/internal/cmd/loadbalancer/describe.go
@@ -12,13 +12,13 @@ import (
 )
 
 // DescribeCmd defines a command for describing a LoadBalancer.
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.LoadBalancer]{
 	ResourceNameSingular: "Load Balancer",
 	ShortDescription:     "Describe a Load Balancer",
 	JSONKeyGetByID:       "load_balancer",
 	JSONKeyGetByName:     "load_balancers",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.LoadBalancer().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.LoadBalancer, any, error) {
 		lb, _, err := s.Client().LoadBalancer().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
@@ -28,9 +28,8 @@ var DescribeCmd = base.DescribeCmd{
 	AdditionalFlags: func(cmd *cobra.Command) {
 		cmd.Flags().Bool("expand-targets", false, "Expand all label_selector targets")
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
+	PrintText: func(s state.State, cmd *cobra.Command, loadBalancer *hcloud.LoadBalancer) error {
 		withLabelSelectorTargets, _ := cmd.Flags().GetBool("expand-targets")
-		loadBalancer := resource.(*hcloud.LoadBalancer)
 		cmd.Printf("ID:\t\t\t\t%d\n", loadBalancer.ID)
 		cmd.Printf("Name:\t\t\t\t%s\n", loadBalancer.Name)
 		cmd.Printf("Created:\t\t\t%s (%s)\n", util.Datetime(loadBalancer.Created), humanize.Time(loadBalancer.Created))
diff --git a/internal/cmd/loadbalancertype/describe.go b/internal/cmd/loadbalancertype/describe.go
index 5c3ea10..5561f32 100644
--- a/internal/cmd/loadbalancertype/describe.go
+++ b/internal/cmd/loadbalancertype/describe.go
@@ -11,22 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.LoadBalancerType]{
 	ResourceNameSingular: "Load Balancer Type",
 	ShortDescription:     "Describe a Load Balancer type",
 	JSONKeyGetByID:       "load_balancer_type",
 	JSONKeyGetByName:     "load_balancer_types",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.LoadBalancerType().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.LoadBalancerType, any, error) {
 		lbt, _, err := s.Client().LoadBalancerType().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return lbt, hcloud.SchemaFromLoadBalancerType(lbt), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		loadBalancerType := resource.(*hcloud.LoadBalancerType)
-
+	PrintText: func(s state.State, cmd *cobra.Command, loadBalancerType *hcloud.LoadBalancerType) error {
 		cmd.Printf("ID:\t\t\t\t%d\n", loadBalancerType.ID)
 		cmd.Printf("Name:\t\t\t\t%s\n", loadBalancerType.Name)
 		cmd.Printf("Description:\t\t\t%s\n", loadBalancerType.Description)
diff --git a/internal/cmd/location/describe.go b/internal/cmd/location/describe.go
index e72ef0e..294eddd 100644
--- a/internal/cmd/location/describe.go
+++ b/internal/cmd/location/describe.go
@@ -10,22 +10,20 @@ import (
 )
 
 // DescribeCmd defines a command for describing a location.
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Location]{
 	ResourceNameSingular: "location",
 	ShortDescription:     "Describe a location",
 	JSONKeyGetByID:       "location",
 	JSONKeyGetByName:     "locations",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Location().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Location, any, error) {
 		l, _, err := s.Client().Location().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return l, hcloud.SchemaFromLocation(l), nil
 	},
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		location := resource.(*hcloud.Location)
-
+	PrintText: func(_ state.State, cmd *cobra.Command, location *hcloud.Location) error {
 		cmd.Printf("ID:\t\t%d\n", location.ID)
 		cmd.Printf("Name:\t\t%s\n", location.Name)
 		cmd.Printf("Description:\t%s\n", location.Description)
diff --git a/internal/cmd/network/describe.go b/internal/cmd/network/describe.go
index c599892..37d6dc3 100644
--- a/internal/cmd/network/describe.go
+++ b/internal/cmd/network/describe.go
@@ -12,22 +12,20 @@ import (
 )
 
 // DescribeCmd defines a command for describing a network.
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Network]{
 	ResourceNameSingular: "network",
 	ShortDescription:     "Describe a network",
 	JSONKeyGetByID:       "network",
 	JSONKeyGetByName:     "networks",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Network().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Network, any, error) {
 		n, _, err := s.Client().Network().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return n, hcloud.SchemaFromNetwork(n), nil
 	},
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		network := resource.(*hcloud.Network)
-
+	PrintText: func(_ state.State, cmd *cobra.Command, network *hcloud.Network) error {
 		cmd.Printf("ID:\t\t%d\n", network.ID)
 		cmd.Printf("Name:\t\t%s\n", network.Name)
 		cmd.Printf("Created:\t%s (%s)\n", util.Datetime(network.Created), humanize.Time(network.Created))
diff --git a/internal/cmd/placementgroup/describe.go b/internal/cmd/placementgroup/describe.go
index 23de63e..8b9161a 100644
--- a/internal/cmd/placementgroup/describe.go
+++ b/internal/cmd/placementgroup/describe.go
@@ -11,22 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.PlacementGroup]{
 	ResourceNameSingular: "placement group",
 	ShortDescription:     "Describe a placement group",
 	JSONKeyGetByID:       "placement_group",
 	JSONKeyGetByName:     "placement_groups",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.PlacementGroup().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.PlacementGroup, any, error) {
 		pg, _, err := s.Client().PlacementGroup().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return pg, hcloud.SchemaFromPlacementGroup(pg), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		placementGroup := resource.(*hcloud.PlacementGroup)
-
+	PrintText: func(s state.State, cmd *cobra.Command, placementGroup *hcloud.PlacementGroup) error {
 		cmd.Printf("ID:\t\t%d\n", placementGroup.ID)
 		cmd.Printf("Name:\t\t%s\n", placementGroup.Name)
 		cmd.Printf("Created:\t%s (%s)\n", util.Datetime(placementGroup.Created), humanize.Time(placementGroup.Created))
diff --git a/internal/cmd/primaryip/describe.go b/internal/cmd/primaryip/describe.go
index 670628d..6c274f9 100644
--- a/internal/cmd/primaryip/describe.go
+++ b/internal/cmd/primaryip/describe.go
@@ -11,22 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.PrimaryIP]{
 	ResourceNameSingular: "Primary IP",
 	ShortDescription:     "Describe an Primary IP",
 	JSONKeyGetByID:       "primary_ip",
 	JSONKeyGetByName:     "primary_ips",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.PrimaryIP().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.PrimaryIP, any, error) {
 		ip, _, err := s.Client().PrimaryIP().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return ip, hcloud.SchemaFromPrimaryIP(ip), nil
 	},
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		primaryIP := resource.(*hcloud.PrimaryIP)
-
+	PrintText: func(_ state.State, cmd *cobra.Command, primaryIP *hcloud.PrimaryIP) error {
 		cmd.Printf("ID:\t\t%d\n", primaryIP.ID)
 		cmd.Printf("Name:\t\t%s\n", primaryIP.Name)
 		cmd.Printf("Created:\t%s (%s)\n", util.Datetime(primaryIP.Created), humanize.Time(primaryIP.Created))
diff --git a/internal/cmd/server/describe.go b/internal/cmd/server/describe.go
index f775390..b7a593a 100644
--- a/internal/cmd/server/describe.go
+++ b/internal/cmd/server/describe.go
@@ -13,22 +13,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Server]{
 	ResourceNameSingular: "server",
 	ShortDescription:     "Describe a server",
 	JSONKeyGetByID:       "server",
 	JSONKeyGetByName:     "servers",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Server().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Server, any, error) {
 		srv, _, err := s.Client().Server().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return srv, hcloud.SchemaFromServer(srv), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		server := resource.(*hcloud.Server)
-
+	PrintText: func(s state.State, cmd *cobra.Command, server *hcloud.Server) error {
 		cmd.Printf("ID:\t\t%d\n", server.ID)
 		cmd.Printf("Name:\t\t%s\n", server.Name)
 		cmd.Printf("Status:\t\t%s\n", server.Status)
diff --git a/internal/cmd/servertype/describe.go b/internal/cmd/servertype/describe.go
index 00f749f..29510e2 100644
--- a/internal/cmd/servertype/describe.go
+++ b/internal/cmd/servertype/describe.go
@@ -11,22 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.ServerType]{
 	ResourceNameSingular: "serverType",
 	ShortDescription:     "Describe a server type",
 	JSONKeyGetByID:       "server_type",
 	JSONKeyGetByName:     "server_types",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.ServerType().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.ServerType, any, error) {
 		st, _, err := s.Client().ServerType().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return st, hcloud.SchemaFromServerType(st), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		serverType := resource.(*hcloud.ServerType)
-
+	PrintText: func(s state.State, cmd *cobra.Command, serverType *hcloud.ServerType) error {
 		cmd.Printf("ID:\t\t\t%d\n", serverType.ID)
 		cmd.Printf("Name:\t\t\t%s\n", serverType.Name)
 		cmd.Printf("Description:\t\t%s\n", serverType.Description)
diff --git a/internal/cmd/sshkey/describe.go b/internal/cmd/sshkey/describe.go
index 9217587..024276c 100644
--- a/internal/cmd/sshkey/describe.go
+++ b/internal/cmd/sshkey/describe.go
@@ -13,21 +13,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.SSHKey]{
 	ResourceNameSingular: "SSH Key",
 	ShortDescription:     "Describe a SSH Key",
 	JSONKeyGetByID:       "ssh_key",
 	JSONKeyGetByName:     "ssh_keys",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.SSHKey().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.SSHKey, any, error) {
 		key, _, err := s.Client().SSHKey().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return key, hcloud.SchemaFromSSHKey(key), nil
 	},
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		sshKey := resource.(*hcloud.SSHKey)
+	PrintText: func(_ state.State, cmd *cobra.Command, sshKey *hcloud.SSHKey) error {
 		cmd.Printf("ID:\t\t%d\n", sshKey.ID)
 		cmd.Printf("Name:\t\t%s\n", sshKey.Name)
 		cmd.Printf("Created:\t%s (%s)\n", util.Datetime(sshKey.Created), humanize.Time(sshKey.Created))
diff --git a/internal/cmd/volume/describe.go b/internal/cmd/volume/describe.go
index cebe5d2..f99d4f2 100644
--- a/internal/cmd/volume/describe.go
+++ b/internal/cmd/volume/describe.go
@@ -11,22 +11,20 @@ import (
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 )
 
-var DescribeCmd = base.DescribeCmd{
+var DescribeCmd = base.DescribeCmd[*hcloud.Volume]{
 	ResourceNameSingular: "volume",
 	ShortDescription:     "Describe an Volume",
 	JSONKeyGetByID:       "volume",
 	JSONKeyGetByName:     "volumes",
 	NameSuggestions:      func(c hcapi2.Client) func() []string { return c.Volume().Names },
-	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (interface{}, interface{}, error) {
+	Fetch: func(s state.State, _ *cobra.Command, idOrName string) (*hcloud.Volume, any, error) {
 		v, _, err := s.Client().Volume().Get(s, idOrName)
 		if err != nil {
 			return nil, nil, err
 		}
 		return v, hcloud.SchemaFromVolume(v), nil
 	},
-	PrintText: func(s state.State, cmd *cobra.Command, resource interface{}) error {
-		volume := resource.(*hcloud.Volume)
-
+	PrintText: func(s state.State, cmd *cobra.Command, volume *hcloud.Volume) error {
 		cmd.Printf("ID:\t\t%d\n", volume.ID)
 		cmd.Printf("Name:\t\t%s\n", volume.Name)
 		cmd.Printf("Created:\t%s (%s)\n", util.Datetime(volume.Created), humanize.Time(volume.Created))


Test patch:
diff --git a/internal/cmd/base/describe_test.go b/internal/cmd/base/describe_test.go
index dad57bf..c973857 100644
--- a/internal/cmd/base/describe_test.go
+++ b/internal/cmd/base/describe_test.go
@@ -12,10 +12,10 @@ import (
 	"github.com/hetznercloud/cli/internal/testutil"
 )
 
-var fakeDescribeCmd = &base.DescribeCmd{
+var fakeDescribeCmd = &base.DescribeCmd[*fakeResource]{
 	ResourceNameSingular: "Fake resource",
 
-	Fetch: func(_ state.State, cmd *cobra.Command, _ string) (interface{}, interface{}, error) {
+	Fetch: func(_ state.State, cmd *cobra.Command, _ string) (*fakeResource, any, error) {
 		cmd.Println("Fetching fake resource")
 
 		resource := &fakeResource{
@@ -26,8 +26,7 @@ var fakeDescribeCmd = &base.DescribeCmd{
 		return resource, util.Wrap("resource", resource), nil
 	},
 
-	PrintText: func(_ state.State, cmd *cobra.Command, resource interface{}) error {
-		rsc := resource.(*fakeResource)
+	PrintText: func(_ state.State, cmd *cobra.Command, rsc *fakeResource) error {
 		cmd.Printf("ID: %d\n", rsc.ID)
 		cmd.Printf("Name: %s\n", rsc.Name)
 		return nil