Repository: django-components/django-components
Pull request #1125: refactor: use typevar defaults + raise on conflicting extensions
Author: login: JuroOravec
Created: 2025-04-14T07:48:48Z | Merged: 2025-04-14T08:00:18Z

When typing a Component, one can now specify as few or as many parameters as you want.

```py
Component[Args]
Component[Args, Kwargs]
Component[Args, Kwargs, Slots]
Component[Args, Kwargs, Slots, Data]
Component[Args, Kwargs, Slots, Data, JsData]
Component[Args, Kwargs, Slots, Data, JsData, CssData]
```

All omitted parameters will default to `Any`.

Closes #1113 

Linked issue #1113: Make dynamic typing declaration with TypeVar from typings_extensions
In I think either 3.12 or 3.13, it should be possible to define generics (`TypeVar`) with defaults ([docs](https://typing.python.org/en/latest/spec/generics.html#defaults-for-type-parameters)). In those case, the type parameter can be omitted from the type declaration.

Currently, when we define the type for the Component class, we have to specify all 6 types at once:

```py
Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]
```

which is a mouthful, because when I want to only e.g. args and kwargs, I still have to set the rest to `Any`:

```py
Component[MyArgs, MyKwargs, Any, Any, Any, Any]
```

If we added defaults, this would make the declarations shorter. Any of the following would be allowed:

```py
Component
Component[ArgsType]
Component[ArgsType, KwargsType]
Component[ArgsType, KwargsType, SlotsType]
Component[ArgsType, KwargsType, SlotsType, DataType]
Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType]
Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]
```

Since we support Python versions all the way down to 3.8*, simply waiting until we support only Python 3.12 would take many years.

Instead, I just learnt that it's possible to install [typing-extensions](https://pypi.org/project/typing-extensions/) on older versions to back-port the new typing-related features.

I haven't checked yet, but if works also on Py3.8, that'd be really nice.

The only thing is that this would have to be declared as a package dependency. But since this is a Python's own package, I don't see an issue with that.

@EmilStenstrom What do you think?

---

\* We should support Python 3.8 until Apr 2026 as that's when support for [Django 4.2 ends](https://endoflife.date/django).


Linked issue comments: user: login: JuroOravec
created_at: 2025-04-10T07:10:13Z
body: Can confrm that it works with Python 3.8!

<img width="458" alt="Image" src="https://github.com/user-attachments/assets/87e05860-e533-4aae-83a3-06b217cac9ec" />
author_association: COLLABORATOR
user: login: EmilStenstrom
created_at: 2025-04-10T08:34:09Z
body: @JuroOravec I've never been that deep into typing, but I know that many projects already depend on typing_extensions for old python versions, so I'm 👍 on us doing it too!
author_association: COLLABORATOR

Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a69c573fc..17e951c07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,32 @@
 # Release notes
 
-## v0.139
+## v0.139.1
+
+#### Refactor
+
+- When typing a Component, you can now specify as few or as many parameters as you want.
+
+    ```py
+    Component[Args]
+    Component[Args, Kwargs]
+    Component[Args, Kwargs, Slots]
+    Component[Args, Kwargs, Slots, Data]
+    Component[Args, Kwargs, Slots, Data, JsData]
+    Component[Args, Kwargs, Slots, Data, JsData, CssData]
+    ```
+
+    All omitted parameters will default to `Any`.
+
+- Added `typing_extensions` to the project as a dependency
+
+- Multiple extensions with the same name (case-insensitive) now raise an error
+
+- Extension names (case-insensitive) also MUST NOT conflict with existing Component class API.
+  
+    So if you name an extension `render`, it will conflict with the `render()` method of the `Component` class,
+    and thus raise an error.
+
+## v0.139.0
 
 #### Fix
 
diff --git a/README.md b/README.md
index 0a13fc3dc..7761df288 100644
--- a/README.md
+++ b/README.md
@@ -396,7 +396,7 @@ Components API is fully typed, and supports [static type hints](https://django-c
 To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
 
 ```py
-from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
+from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
 
 from django_components import Component
 
@@ -407,22 +407,19 @@ class ButtonKwargs(TypedDict):
     another: int
     maybe_var: NotRequired[int] # May be omitted
 
-class ButtonData(TypedDict):
-    variable: str
-
 class ButtonSlots(TypedDict):
-    my_slot: NotRequired[SlotFunc]
+    # Use `Slot` for slot functions.
+    my_slot: NotRequired[Slot]
+    # Use `SlotContent` when you want to allow either `Slot` instance or plain string
     another_slot: SlotContent
 
-ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
+ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
 
 class Button(ButtonType):
     def get_context_data(self, *args, **kwargs):
         self.input.args[0]  # int
         self.input.kwargs["variable"]  # str
-        self.input.slots["my_slot"]  # SlotFunc[MySlotData]
-
-        return {}  # Error: Key "variable" is missing
+        self.input.slots["my_slot"]  # Slot[MySlotData]
 ```
 
 When you then call
diff --git a/docs/concepts/advanced/authoring_component_libraries.md b/docs/concepts/advanced/authoring_component_libraries.md
index b1c41e1d9..d3991951a 100644
--- a/docs/concepts/advanced/authoring_component_libraries.md
+++ b/docs/concepts/advanced/authoring_component_libraries.md
@@ -103,7 +103,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
     ```djc_py
     from typing import Dict, NotRequired, Optional, Tuple, TypedDict
 
-    from django_components import Component, SlotFunc, register, types
+    from django_components import Component, SlotContent, register, types
 
     from myapp.templatetags.mytags import comp_registry
 
@@ -114,7 +114,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
     type MyMenuArgs = Tuple[int, str]
 
     class MyMenuSlots(TypedDict):
-        default: NotRequired[Optional[SlotFunc[EmptyDict]]]
+        default: NotRequired[Optional[SlotContent[EmptyDict]]]
 
     class MyMenuProps(TypedDict):
         vertical: NotRequired[bool]
@@ -124,7 +124,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
     # Define the component
     # NOTE: Don't forget to set the `registry`!
     @register("my_menu", registry=comp_registry)
-    class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots, Any, Any, Any]):
+    class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots]):
         def get_context_data(
             self,
             *args,
diff --git a/docs/concepts/advanced/extensions.md b/docs/concepts/advanced/extensions.md
index 36ad39260..97a9a1966 100644
--- a/docs/concepts/advanced/extensions.md
+++ b/docs/concepts/advanced/extensions.md
@@ -169,6 +169,14 @@ class MyExtension(ComponentExtension):
         ctx.component_cls.my_attr = "my_value"
 ```
 
+!!! warning
+
+    The `name` attribute MUST be unique across all extensions.
+
+    Moreover, the `name` attribute MUST NOT conflict with existing Component class API.
+
+    So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
+
 ### Defining the extension class
 
 In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
diff --git a/docs/concepts/advanced/typing_and_validation.md b/docs/concepts/advanced/typing_and_validation.md
index 53d27a9ca..8ac22a54c 100644
--- a/docs/concepts/advanced/typing_and_validation.md
+++ b/docs/concepts/advanced/typing_and_validation.md
@@ -10,7 +10,7 @@ Use this to add type hints to your components, or to validate component inputs.
 ```py
 from django_components import Component
 
-ButtonType = Component[Args, Kwargs, Slots, Data, JsData, CssData]
+ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
 
 class Button(ButtonType):
     template_file = "button.html"
@@ -24,16 +24,34 @@ The generic parameters are:
 - `Args` - Positional arguments, must be a `Tuple` or `Any`
 - `Kwargs` - Keyword arguments, must be a `TypedDict` or `Any`
 - `Slots` - Slots, must be a `TypedDict` or `Any`
-- `Data` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
+- `TemplateData` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
 - `JsData` - Data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), must be a `TypedDict` or `Any`
 - `CssData` - Data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data), must be a `TypedDict` or `Any`
 
-## Example
+You can specify as many or as few of the parameters as you want. The rest will default to `Any`.
+
+All of these are valid:
+
+```py
+ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
+ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData]
+ButtonType = Component[Args, Kwargs, Slots, TemplateData]
+ButtonType = Component[Args, Kwargs, Slots]
+ButtonType = Component[Args, Kwargs]
+ButtonType = Component[Args]
+```
+
+!!! info
+
+    Setting a type parameter to `Any` will disable typing and validation for that part.
+
+## Typing inputs
+
+You can use the first 3 parameters to type inputs.
 
 ```python
-from typing import NotRequired, Tuple, TypedDict
-from pydantic import BaseModel
-from django_components import Component, SlotContent, SlotFunc
+from typing_extensions import NotRequired, Tuple, TypedDict
+from django_components import Component, Slot, SlotContent
 
 ###########################################
 # 1. Define the types
@@ -54,39 +72,17 @@ class ButtonFooterSlotData(TypedDict):
 
 # Slots
 class ButtonSlots(TypedDict):
-    # SlotContent == str or slot func
+    # Use `SlotContent` when you want to allow either `Slot` instance or plain string
     header: SlotContent
-    # Use SlotFunc for slot functions.
+    # Use `Slot` for slot functions.
     # The generic specifies the data available to the slot function
-    footer: NotRequired[SlotFunc[ButtonFooterSlotData]]
-
-# Data returned from `get_context_data`
-class ButtonData(BaseModel):
-    data1: str
-    data2: int
-
-# Data returned from `get_js_data`
-class ButtonJsData(BaseModel):
-    js_data1: str
-    js_data2: int
-
-# Data returned from `get_css_data`
-class ButtonCssData(BaseModel):
-    css_data1: str
-    css_data2: int
+    footer: NotRequired[Slot[ButtonFooterSlotData]]
 
 ###########################################
 # 2. Define the component with those types
 ###########################################
 
-ButtonType = Component[
-    ButtonArgs,
-    ButtonKwargs,
-    ButtonSlots,
-    ButtonData,
-    ButtonJsData,
-    ButtonCssData,
-]
+ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
 
 class Button(ButtonType):
     def get_context_data(self, *args, **kwargs):
@@ -109,23 +105,81 @@ Button.render(
     },
     slots={
         "header": "...",
-        # ERROR: Expects key "footer"
+        "footer": lambda ctx, slot_data, slot_ref: slot_data.value + 1,
+        # ERROR: Unexpected key "foo"
         "foo": "invalid",
     },
 )
 ```
 
+If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any) or omit them.
+
+The following will validate only the keyword inputs:
+
+```python
+ButtonType = Component[Any, ButtonKwargs]
+
+class Button(ButtonType):
+    ...
+```
+
+## Typing data
+
+You can use the last 3 parameters to type the data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), and [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
+
+Use this together with the [Pydantic integration](#runtime-input-validation-with-types) to have runtime validation of the data.
+
+```python
+from typing_extensions import NotRequired, Tuple, TypedDict
+from django_components import Component, Slot, SlotContent
+
+###########################################
+# 1. Define the types
+###########################################
+
+# Data returned from `get_context_data`
+class ButtonData(TypedDict):
+    data1: str
+    data2: int
+
+# Data returned from `get_js_data`
+class ButtonJsData(TypedDict):
+    js_data1: str
+    js_data2: int
+
+# Data returned from `get_css_data`
+class ButtonCssData(TypedDict):
+    css_data1: str
+    css_data2: int
+
+###########################################
+# 2. Define the component with those types
+###########################################
+
+ButtonType = Component[Any, Any, Any, ButtonData, ButtonJsData, ButtonCssData]
+
+class Button(ButtonType):
+    def get_context_data(self, *args, **kwargs):
+        ...
+
+    def get_js_data(self, *args, **kwargs):
+        ...
+
+    def get_css_data(self, *args, **kwargs):
+        ...
+```
+
+When you then call
+[`Component.render`](../../../reference/api#django_components.Component.render)
+or [`Component.render_to_response`](../../../reference/api#django_components.Component.render_to_response),
+the component will raise an error if the data is not valid.
+
 If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any).
 
+The following will validate only the data returned from `get_js_data`:
+
 ```python
-ButtonType = Component[
-    ButtonArgs,
-    ButtonKwargs,
-    ButtonSlots,
-    Any,
-    Any,
-    Any,
-]
+ButtonType = Component[Any, Any, Any, Any, ButtonJsData]
 
 class Button(ButtonType):
     ...
@@ -182,6 +236,9 @@ class Button(Component[EmptyTuple, EmptyDict, EmptyDict, EmptyDict, EmptyDict, E
 
     Since v0.136, input validation is available as a separate extension.
 
+By defualt, when you add types to your component, this will only set up static type hints,
+but it will not validate the inputs.
+
 To enable input validation, you need to install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension:
 
 ```bash
diff --git a/docs/concepts/fundamentals/components_in_python.md b/docs/concepts/fundamentals/components_in_python.md
index 1cf569f13..4fb8bbe99 100644
--- a/docs/concepts/fundamentals/components_in_python.md
+++ b/docs/concepts/fundamentals/components_in_python.md
@@ -47,7 +47,7 @@ Component.render(
     context: Mapping | django.template.Context | None = None,
     args: List[Any] | None = None,
     kwargs: Dict[str, Any] | None = None,
-    slots: Dict[str, str | SafeString | SlotFunc] | None = None,
+    slots: Dict[str, str | SafeString | SlotContent] | None = None,
     escape_slots_content: bool = True
 ) -> str:
 ```
@@ -60,7 +60,7 @@ Component.render(
 
 - _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
   Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
-  or [`SlotFunc`](#slotfunc).
+  or `Slot`.
 
 - _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
 
diff --git a/docs/overview/welcome.md b/docs/overview/welcome.md
index 044d342da..1ac31f207 100644
--- a/docs/overview/welcome.md
+++ b/docs/overview/welcome.md
@@ -386,7 +386,7 @@ Components API is fully typed, and supports [static type hints](https://django-c
 To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
 
 ```py
-from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
+from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
 
 from django_components import Component
 
@@ -397,22 +397,19 @@ class ButtonKwargs(TypedDict):
     another: int
     maybe_var: NotRequired[int] # May be omitted
 
-class ButtonData(TypedDict):
-    variable: str
-
 class ButtonSlots(TypedDict):
-    my_slot: NotRequired[SlotFunc]
+    # Use `Slot` for slot functions.
+    my_slot: NotRequired[Slot]
+    # Use `SlotContent` when you want to allow either `Slot` instance or plain string
     another_slot: SlotContent
 
-ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
+ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
 
 class Button(ButtonType):
     def get_context_data(self, *args, **kwargs):
         self.input.args[0]  # int
         self.input.kwargs["variable"]  # str
-        self.input.slots["my_slot"]  # SlotFunc[MySlotData]
-
-        return {}  # Error: Key "variable" is missing
+        self.input.slots["my_slot"]  # Slot
 ```
 
 When you then call
diff --git a/docs/reference/api.md b/docs/reference/api.md
index 4fc92d8f4..637ddc6c2 100644
--- a/docs/reference/api.md
+++ b/docs/reference/api.md
@@ -19,10 +19,6 @@
     options:
       show_if_no_docstring: true
 
-::: django_components.ComponentCommand
-    options:
-      show_if_no_docstring: true
-
 ::: django_components.ComponentDefaults
     options:
       show_if_no_docstring: true
@@ -35,6 +31,10 @@
     options:
       show_if_no_docstring: true
 
+::: django_components.ComponentInput
+    options:
+      show_if_no_docstring: true
+
 ::: django_components.ComponentMediaInput
     options:
       show_if_no_docstring: true
diff --git a/docs/reference/commands.md b/docs/reference/commands.md
index 5886d02f8..c27716707 100644
--- a/docs/reference/commands.md
+++ b/docs/reference/commands.md
@@ -462,8 +462,8 @@ ProjectDashboardAction    project.components.dashboard_action.ProjectDashboardAc
 ## `upgradecomponent`
 
 ```txt
-usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
-                        [--traceback] [--no-color] [--force-color] [--skip-checks]
+usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
+                        [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
 
 ```
 
@@ -507,9 +507,9 @@ Deprecated. Use `components upgrade` instead.
 ## `startcomponent`
 
 ```txt
-usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
-                      [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
-                      [--no-color] [--force-color] [--skip-checks]
+usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
+                      [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
+                      [--traceback] [--no-color] [--force-color] [--skip-checks]
                       name
 
 ```
diff --git a/docs/reference/extension_hooks.md b/docs/reference/extension_hooks.md
index 38b48256a..0c241514c 100644
--- a/docs/reference/extension_hooks.md
+++ b/docs/reference/extension_hooks.md
@@ -109,6 +109,7 @@ name | type | description
 
 ::: django_components.extension.ComponentExtension.on_component_rendered
     options:
+      heading_level: 3
       show_root_heading: true
       show_signature: true
       separate_signature: true
diff --git a/docs/reference/template_tags.md b/docs/reference/template_tags.md
index 1ec60bab4..ef14eb0b6 100644
--- a/docs/reference/template_tags.md
+++ b/docs/reference/template_tags.md
@@ -20,7 +20,7 @@ Import as
 
 
 
-<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1037" target="_blank">See source code</a>
+<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1064" target="_blank">See source code</a>
 
 
 
@@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
 
 
 
-<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1059" target="_blank">See source code</a>
+<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1086" target="_blank">See source code</a>
 
 
 
@@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
 
 
 
-<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1564" target="_blank">See source code</a>
+<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1669" target="_blank">See source code</a>
 
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 25069b08e..e726920b4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ classifiers = [
 dependencies = [
     'Django>=4.2',
     'djc-core-html-parser>=1.0.2',
+    'typing-extensions>=4.12.2',
 ]
 license = {text = "MIT"}
 
diff --git a/requirements-ci.in b/requirements-ci.in
index dad5079ea..f0a40de40 100644
--- a/requirements-ci.in
+++ b/requirements-ci.in
@@ -6,4 +6,5 @@ types-requests
 whitenoise
 asv
 pytest-asyncio
-pytest-django
\ No newline at end of file
+pytest-django
+typing-extensions>=4.12.2
\ No newline at end of file
diff --git a/requirements-ci.txt b/requirements-ci.txt
index 6518b8879..32ab16270 100644
--- a/requirements-ci.txt
+++ b/requirements-ci.txt
@@ -95,6 +95,7 @@ types-requests==2.32.0.20241016
     # via -r requirements-ci.in
 typing-extensions==4.12.2
     # via
+    #   -r requirements-ci.in
     #   pyee
     #   tox
 urllib3==2.2.3
diff --git a/requirements-dev.in b/requirements-dev.in
index 83cda366e..30ec419bf 100644
--- a/requirements-dev.in
+++ b/requirements-dev.in
@@ -17,4 +17,5 @@ types-requests
 whitenoise
 pygments
 pygments-djc
-asv
\ No newline at end of file
+asv
+typing-extensions>=4.12.2
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 1d9bcc989..4c6eea126 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -150,6 +150,7 @@ types-requests==2.32.0.20241016
     # via -r requirements-dev.in
 typing-extensions==4.12.2
     # via
+    #   -r requirements-dev.in
     #   asgiref
     #   black
     #   mypy
diff --git a/src/django_components/component.py b/src/django_components/component.py
index 08d5c47c9..68ed37ba0 100644
--- a/src/django_components/component.py
+++ b/src/django_components/component.py
@@ -18,7 +18,6 @@
     Optional,
     Tuple,
     Type,
-    TypeVar,
     Union,
     cast,
 )
@@ -34,6 +33,7 @@
 from django.test.signals import template_rendered
 from django.utils.html import conditional_escape
 from django.views import View
+from typing_extensions import TypeVar
 
 from django_components.app_settings import ContextBehavior, app_settings
 from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
@@ -103,12 +103,12 @@
 COMP_ONLY_FLAG = "only"
 
 # Define TypeVars for args and kwargs
-ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
-KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True)
-SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent])
-DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True)
-JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any])
-CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
+ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True, default=Any)
+KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True, default=Any)
+SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent], default=Any)
+DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True, default=Any)
+JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any], default=Any)
+CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any], default=Any)
 
 
 # NOTE: `ReferenceType` is NOT a generic pre-3.9
diff --git a/src/django_components/extension.py b/src/django_components/extension.py
index 12c38beb5..0ccf06c81 100644
--- a/src/django_components/extension.py
+++ b/src/django_components/extension.py
@@ -1,5 +1,5 @@
 from functools import wraps
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Tuple, Type, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Type, TypeVar, Union
 
 import django.urls
 from django.template import Context
@@ -661,7 +661,20 @@ def _init_app(self) -> None:
         # Populate the `urlpatterns` with URLs specified by the extensions
         # TODO_V3 - Django-specific logic - replace with hook
         urls: List[URLResolver] = []
+        seen_names: Set[str] = set()
+
+        from django_components import Component
+
         for extension in self.extensions:
+            # Ensure that the extension name won't conflict with existing Component class API
+            if hasattr(Component, extension.name) or hasattr(Component, extension.class_name):
+                raise ValueError(f"Extension name '{extension.name}' conflicts with existing Component class API")
+
+            if extension.name.lower() in seen_names:
+                raise ValueError(f"Multiple extensions cannot have the same name '{extension.name}'")
+
+            seen_names.add(extension.name.lower())
+
             # NOTE: The empty list is a placeholder for the URLs that will be added later
             curr_ext_url_resolver = django.urls.path(f"{extension.name}/", django.urls.include([]))
             urls.append(curr_ext_url_resolver)
diff --git a/src/django_components/template.py b/src/django_components/template.py
index 5dad8dac6..909e2b115 100644
--- a/src/django_components/template.py
+++ b/src/django_components/template.py
@@ -1,12 +1,10 @@
-from typing import Any, Optional, Type, TypeVar
+from typing import Any, Optional, Type
 
 from django.template import Origin, Template
 
 from django_components.cache import get_template_cache
 from django_components.util.misc import get_import_path
 
-TTemplate = TypeVar("TTemplate", bound=Template)
-
 
 # Central logic for creating Templates from string, so we can cache the results
 def cached_template(
diff --git a/tests/test_component.py b/tests/test_component.py
index b7587f915..4a763200c 100644
--- a/tests/test_component.py
+++ b/tests/test_component.py
@@ -4,7 +4,8 @@
 """
 
 import re
-from typing import Any, Dict, no_type_check
+from typing import Any, Dict, Tuple, no_type_check
+from typing_extensions import NotRequired, TypedDict
 
 import pytest
 from django.conf import settings
@@ -16,7 +17,16 @@
 from django.urls import path
 from pytest_django.asserts import assertHTMLEqual, assertInHTML
 
-from django_components import Component, ComponentView, all_components, get_component_by_class_id, register, types
+from django_components import (
+    Component,
+    ComponentView,
+    SlotContent,
+    Slot,
+    all_components,
+    get_component_by_class_id,
+    register,
+    types,
+)
 from django_components.slots import SlotRef
 from django_components.urls import urlpatterns as dc_urlpatterns
 
@@ -375,6 +385,101 @@ def get_context_data(self):
 
         assert SimpleComponent.render() == "Hello"
 
+    def test_typing(self):
+        # Types
+        ButtonArgs = Tuple[str, ...]
+
+        class ButtonKwargs(TypedDict):
+            name: str
+            age: int
+            maybe_var: NotRequired[int]
+
+        class ButtonFooterSlotData(TypedDict):
+            value: int
+
+        class ButtonSlots(TypedDict):
+            # Use `SlotContent` when you want to allow either function (`Slot` instance)
+            # or plain string.
+            header: SlotContent
+            # Use `Slot` for slot functions. The generic specifies the data available to the slot function.
+            footer: NotRequired[Slot[ButtonFooterSlotData]]
+
+        # Data returned from `get_context_data`
+        class ButtonData(TypedDict):
+            data1: str
+            data2: int
+
+        # Data returned from `get_js_data`
+        class ButtonJsData(TypedDict):
+            js_data1: str
+            js_data2: int
+
+        # Data returned from `get_css_data`
+        class ButtonCssData(TypedDict):
+            css_data1: str
+            css_data2: int
+
+        # Tests - We simply check that these don't raise any errors
+        #         nor any type errors.
+        ButtonType1 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData, ButtonCssData]
+        ButtonType2 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData]
+        ButtonType3 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData]
+        ButtonType4 = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
+        ButtonType5 = Component[ButtonArgs, ButtonKwargs]
+        ButtonType6 = Component[ButtonArgs]
+
+        class Button1(ButtonType1):
+            template = "<button>Click me!</button>"
+
+        class Button2(ButtonType2):
+            template = "<button>Click me!</button>"
+
+        class Button3(ButtonType3):
+            template = "<button>Click me!</button>"
+
+        class Button4(ButtonType4):
+            template = "<button>Click me!</button>"
+
+        class Button5(ButtonType5):
+            template = "<button>Click me!</button>"
+
+        class Button6(ButtonType6):
+            template = "<button>Click me!</button>"
+
+        Button1.render(
+            args=("arg1", "arg2"),
+            kwargs={"name": "name", "age": 123},
+            slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
+        )
+
+        Button2.render(
+            args=("arg1", "arg2"),
+            kwargs={"name": "name", "age": 123},
+            slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
+        )
+
+        Button3.render(
+            args=("arg1", "arg2"),
+            kwargs={"name": "name", "age": 123},
+            slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
+        )
+
+        Button4.render(
+            args=("arg1", "arg2"),
+            kwargs={"name": "name", "age": 123},
+            slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
+        )
+
+        Button5.render(
+            args=("arg1", "arg2"),
+            kwargs={"name": "name", "age": 123},
+        )
+
+        Button6.render(
+            args=("arg1", "arg2"),
+        )
+
+
 @djc_test
 class TestComponentRender:
     @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
diff --git a/tests/test_extension.py b/tests/test_extension.py
index 740a6894d..ee9fc8fef 100644
--- a/tests/test_extension.py
+++ b/tests/test_extension.py
@@ -1,6 +1,7 @@
 import gc
 from typing import Any, Callable, Dict, List, cast
 
+import pytest
 from django.http import HttpRequest, HttpResponse
 from django.template import Context
 from django.test import Client
@@ -108,6 +109,10 @@ class DummyNestedExtension(ComponentExtension):
     ]
 
 
+class RenderExtension(ComponentExtension):
+    name = "render"
+
+
 def with_component_cls(on_created: Callable):
     class TempComponent(Component):
         template = "Hello {{ name }}!"
@@ -151,6 +156,22 @@ def get_context_data(self, arg1, arg2, name="World"):
         del TestAccessComp
         gc.collect()
 
+    def test_raises_on_extension_name_conflict(self):
+        @djc_test(components_settings={"extensions": [RenderExtension]})
+        def inner():
+            pass
+
+        with pytest.raises(ValueError, match="Extension name 'render' conflicts with existing Component class API"):
+            inner()
+
+    def test_raises_on_multiple_extensions_with_same_name(self):
+        @djc_test(components_settings={"extensions": [DummyExtension, DummyExtension]})
+        def inner():
+            pass
+
+        with pytest.raises(ValueError, match="Multiple extensions cannot have the same name 'test_extension'"):
+            inner()
+
 
 @djc_test
 class TestExtensionHooks:


Reviews:
user: login: JuroOravec
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T07:56:38Z
user: login: JuroOravec
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T07:57:30Z
user: login: JuroOravec
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T07:59:14Z

Review comments:
diff_hunk: @@ -407,22 +407,19 @@ class ButtonKwargs(TypedDict):
     another: int
     maybe_var: NotRequired[int] # May be omitted
 
-class ButtonData(TypedDict):
-    variable: str
-
 class ButtonSlots(TypedDict):
-    my_slot: NotRequired[SlotFunc]
+    # Use `Slot` for slot functions.
+    my_slot: NotRequired[Slot]
+    # Use `SlotContent` when you want to allow either `Slot` instance or plain string
     another_slot: SlotContent
 
-ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
+ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
path: README.md
user: login: JuroOravec
body: While at it I also updated the docs on typing and validation (note: that page still has work to do). And in the README, to simplify things, I changed it to show only input validation (using only the first 3 type params).
created_at: 2025-04-14T07:56:38Z
author_association: COLLABORATOR
diff_hunk: @@ -169,6 +169,14 @@ class MyExtension(ComponentExtension):
         ctx.component_cls.my_attr = "my_value"
 ```
 
+!!! warning
+
+    The `name` attribute MUST be unique across all extensions.
path: docs/concepts/advanced/extensions.md
user: login: JuroOravec
body: Unrelated to the linked issue, but added validation to extensions so that if there's duplicate name, or the name conflicts with any attributes or methods on the Component class, then we raise error.
created_at: 2025-04-14T07:57:30Z
author_association: COLLABORATOR
diff_hunk: @@ -103,12 +103,12 @@
 COMP_ONLY_FLAG = "only"
 
 # Define TypeVars for args and kwargs
-ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
-KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True)
-SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent])
-DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True)
-JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any])
-CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
+ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True, default=Any)
path: src/django_components/component.py
user: login: JuroOravec
body: This is what allows us to omit some of the types when configuring `Component[...]`
created_at: 2025-04-14T07:59:13Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-14T07:50:49Z
body: ## Performance Benchmark Results

Comparing PR changes against master branch:


Benchmarks that have stayed the same:

| Change   | Before [61528ef0]    | After [058d6d7b]    |   Ratio | Benchmark (Parameter)                                             |
|----------|----------------------|---------------------|---------|-------------------------------------------------------------------|
|          | 73.1±0.7ms           | 72.8±0.7ms          |       1 | Components vs Django.timeraw_render_lg_first('django')            |
|          | 262±3ms              | 262±3ms             |       1 | Components vs Django.timeraw_render_lg_first('django-components') |

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-14T07:54:29Z
body: ## Performance Benchmark Results

Comparing PR changes against master branch:


Benchmarks that have stayed the same:

| Change   | Before [61528ef0]    | After [a85a2765]    |   Ratio | Benchmark (Parameter)                                             |
|----------|----------------------|---------------------|---------|-------------------------------------------------------------------|
|          | 74.0±0.5ms           | 74.0±0.8ms          |       1 | Components vs Django.timeraw_render_lg_first('django')            |
|          | 268±1ms              | 268±3ms             |       1 | Components vs Django.timeraw_render_lg_first('django-components') |

author_association: CONTRIBUTOR
user: login: JuroOravec
created_at: 2025-04-14T08:00:06Z
body: cc @EmilStenstrom 
author_association: COLLABORATOR