diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs
index 62eccf4752d..a2255508e36 100644
--- a/crates/hir/src/code_model.rs
+++ b/crates/hir/src/code_model.rs
@@ -5,9 +5,7 @@ use arrayvec::ArrayVec;
 use base_db::{CrateDisplayName, CrateId, Edition, FileId};
 use either::Either;
 use hir_def::{
-    adt::ReprKind,
-    adt::StructKind,
-    adt::VariantData,
+    adt::{ReprKind, StructKind, VariantData},
     builtin_type::BuiltinType,
     expr::{BindingAnnotation, LabelId, Pat, PatId},
     import_map,
@@ -31,7 +29,7 @@ use hir_expand::{
 };
 use hir_ty::{
     autoderef,
-    display::{HirDisplayError, HirFormatter},
+    display::{write_bounds_like_dyn_trait, HirDisplayError, HirFormatter},
     method_resolution,
     traits::{FnTrait, Solution, SolutionVariables},
     ApplicationTy, BoundVar, CallableDefId, Canonical, DebruijnIndex, FnSig, GenericPredicate,
@@ -1293,6 +1291,20 @@ impl TypeParam {
     }
 }
 
+impl HirDisplay for TypeParam {
+    fn hir_fmt(&self, f: &mut HirFormatter) -> Result<(), HirDisplayError> {
+        write!(f, "{}", self.name(f.db))?;
+        let bounds = f.db.generic_predicates_for_param(self.id);
+        let substs = Substs::type_params(f.db, self.id.parent);
+        let predicates = bounds.iter().cloned().map(|b| b.subst(&substs)).collect::<Vec<_>>();
+        if !(predicates.is_empty() || f.omit_verbose_types()) {
+            write!(f, ": ")?;
+            write_bounds_like_dyn_trait(&predicates, f)?;
+        }
+        Ok(())
+    }
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 pub struct LifetimeParam {
     pub(crate) id: LifetimeParamId,
diff --git a/crates/hir_ty/src/display.rs b/crates/hir_ty/src/display.rs
index 0e827a29e78..a54225c18ba 100644
--- a/crates/hir_ty/src/display.rs
+++ b/crates/hir_ty/src/display.rs
@@ -595,7 +595,7 @@ impl HirDisplay for FnSig {
     }
 }
 
-fn write_bounds_like_dyn_trait(
+pub fn write_bounds_like_dyn_trait(
     predicates: &[GenericPredicate],
     f: &mut HirFormatter,
 ) -> Result<(), HirDisplayError> {
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 2737c900f4c..61439ae5341 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -370,8 +370,9 @@ fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> {
         }
         Definition::Label(it) => Some(Markup::fenced_block(&it.name(db))),
         Definition::LifetimeParam(it) => Some(Markup::fenced_block(&it.name(db))),
-        Definition::TypeParam(_) | Definition::ConstParam(_) => {
-            // FIXME: Hover for generic param
+        Definition::TypeParam(type_param) => Some(Markup::fenced_block(&type_param.display(db))),
+        Definition::ConstParam(_) => {
+            // FIXME: Hover for generic const param
             None
         }
     };
@@ -3257,4 +3258,51 @@ fn foo() {
             "#]],
         );
     }
+
+    #[test]
+    fn hover_type_param() {
+        check(
+            r#"
+struct Foo<T>(T);
+trait Copy {}
+trait Clone {}
+trait Sized {}
+impl<T: Copy + Clone> Foo<T<|>> where T: Sized {}
+"#,
+            expect![[r#"
+                *T*
+
+                ```rust
+                T: Copy + Clone + Sized
+                ```
+            "#]],
+        );
+        check(
+            r#"
+struct Foo<T>(T);
+impl<T> Foo<T<|>> {}
+"#,
+            expect![[r#"
+                *T*
+
+                ```rust
+                T
+                ```
+                "#]],
+        );
+        // lifetimes aren't being substituted yet
+        check(
+            r#"
+struct Foo<T>(T);
+impl<T: 'static> Foo<T<|>> {}
+"#,
+            expect![[r#"
+                *T*
+
+                ```rust
+                T: {error}
+                ```
+                "#]],
+        );
+    }
 }