This is a draft specification for a practical, expressive type notation for Emacs Lisp. It aims to make dynamic code more reliable by combining TypeScript and PHPStan experience with Lisp best practices.
The current draft lives in typespec.md.
Type-level evaluation semantics (guards, assertions, and conditional
return types) are documented in type-level-evaluation.md.
How the bundled typespec-eval evaluator conforms to the spec — including
known gaps and soundness bugs — is tracked in
conformance.md.
Warning
This is an early draft. A type-level evaluator (typespec-eval) exists for
constant folding and inference, but there is no full type checker yet; see
conformance.md for status. For property-based checking,
ert-fnspec-check can be used.
typespec.el provides the typespec macro. It is intentionally small and meant to be used as a compile-time helper, so prefer:
(eval-when-compile
(require 'typespec))The macro stores a literal typespec on the function symbol and has no runtime execution logic, so annotating functions directly in implementation files should have negligible runtime cost.
Simple function type:
(eval-when-compile
(require 'typespec))
(defun my-identity (argument)
"Return the ARGUMENT unchanged."
argument)
;; Simple and safe, but it cannot express that the output is the same value.
(typespec #'my-identity (function (unknown) unknown))
;; Use :forall to bind a type parameter and return the same type.
(typespec #'my-identity (:forall (a) (function (a) a)))Let's consider a more complex example.
(defun my-times2 (n)
"Return N multiplied by 2."
(+ n 2))
;; This looks reasonable, but it misses literal refinement: (my-times2 42)
;; can be inferred as (const 84), not just integer.
(typespec #'my-times2 (:forall (a) (function ((a number)) a)))
;; Use generalize-signed to widen literal results while preserving sign.
(typespec #'my-times2
(:forall ((a number))
(function (a) (generalize-signed a))))Add (declare (typespec-ftype ...)) inside a function to emit an ftype
declaration derived from typespec annotations.
(typespec #'my-times2
(:forall ((a number))
(function (a) (generalize-signed a))))
(defun my-times2 (n)
"Return N multiplied by 2."
(declare (typespec-ftype (function (number) number)))
;; (declare (ftype (function (number) number)))
(+ n n))Warning
typespec-ftype may become useful for compile-time optimization and
performance, but it is still experimental and not strongly recommended yet.
Use ert-fnspec-check for property-based checking:
(ert-fnspec-check (lambda (xs) (reverse (reverse xs))) '(:xs (list integer))
:test (lambda (actual xs) (equal actual xs)))When a function has a typespec annotation, a typed helper like
ert-fnspec-check-typed should be able to reuse it without repeating the spec:
(typespec #'my-identity (:forall (a) (function (a) a)))
(ert-fnspec-check-typed #'my-identity)This is a planned feature; the implementation does not exist yet.
ert-fnspec-check-typed can also use resolver snapshots embedded in the
typespec record. This allows tests to resolve newer utility types even when
the user's runtime typespec package is older, while still warning about
baseline mismatches.
This package is licensed under GNU General Public License, version 3.
parameterized-ert.el Copyright (C) 2026 USAMI Kenta
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.