Type hints in Clojure have a different purpose to those in Python. In Clojure, type hints are only a mechanism to avoid reflection; their use is solely to improve performance.
So the question "Can I use spec as a type hint?" is actually asking "Will the compiler use specs to avoid reflection?", to which the answer is no. However Spec, and other equivalent libraries, can be used for runtime type checking in Clojure.
Where you'd use a dataclass in Python, you'd generally use a map in Clojure. This requires a little explanation because of the different ways Clojure and Python support data modelling.
Suppose Python you have a dataclass:
@dataclass
class InventoryItem:
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
Then the minimal equivalent Clojure code is simply:
(defn total-cost [{:keys [unit-price quantity-on-hand]}]
(* unit-price quantity-on-hand))
The Clojure example lacks explicit type checking, but that may not be necessary. In Clojure, we can incrementally add more specific checks as necessary. For example, we could add a precondition to check the inputs:
(defn total-cost [{:keys [unit-price quantity-on-hand]}]
{:pre [(float? unit-price) (int? quantity-on-hand)]}
(* unit-price quantity-on-hand))
Clojure spec allows checks to be taken further, with the caveat that keywords must be namespaced. Spec is more granular than classes, as it's interested in single key/value pairs, rather than grouped properties as in a class or struct. With spec, we might declare:
(s/def :inventory.item/name string?)
(s/def :inventory.item/unit-price float?)
(s/def :inventory.item/quantity-on-hand int?)
(defn total-cost [{:inventory.item/keys [unit-price quantity-on-hand]}]
(* unit-price quantity-on-hand))
This doesn't automatically perform type checks, as they may not be necessary. We can add type checks to a function with clojure.spec.test.alpha/instrument, or add them as a precondition using clojure.spec.alpha/valid?.
The validation required depends on the origin of the data. Suppose the inventory item comes from a database with its own schema. In which case, we may be reasonably certain that the types are correct and there's no need for additional confirmation.
Or suppose instead that we read the inventory item from an external source we may not trust. In that case, we'd want to validate it as input, and reject it if the data is invalid. But after we've validated it, we can pass it around internally without further checks.
Perhaps we're worried instead about human error. In this case, we might turn on checking during development and testing, but remove it during production when we're more interested in performance. This is the broad use-case for Spec's instrument function.
Clojure's takes a more nuanced, and possibly unique approach to data, compared to other languages. Understanding how Clojure views data is understanding Clojure as a language.