mmzk-typeid
A TypeID and UUIDv7 implementation for Haskell
https://github.com/MMZK1526/mmzk-typeid
| Stackage Nightly 2026-05-27: | 0.7.1.1 |
| Latest on Hackage: | 0.7.1.1 |
mmzk-typeid-0.7.1.1@sha256:4a6cae7defbfa58f83cd566a2086e7dae4f18f25cb6c7d62dae2996e7c23ab53,4877Module documentation for 0.7.1.1
- Data
- Data.KindID
- Data.TypeID
- Data.UUID
mmzk-typeid
Introduction
A TypeID implementation in Haskell. It is a “type-safe, K-sortable, globally unique identifier” extended on top of UUIDv7.
TypeIDs are canonically encoded as lowercase strings consisting of three parts:
- A type prefix (at most 63 characters in all lowercase snake_case ASCII [a-z_]);
- An underscore ‘_’ separator;
- A 128-bit UUIDv7 encoded as a 26-character string using a modified base32 encoding.
For more information, please check out specification v0.3.0.
It also serves as a (temporary) UUIDv7 implementation in Haskell, since there are no official ones yet.
If you notice any issues or have any suggestions, please feel free to open an issue or contact me via email.
Highlights
In addition to the features provided by TypeID, this implementation also supports:
- Generating TypeIDs in a batch. They are guaranteed to have the same timestamp (up to the first 32768 ids) and of ascending order;
- Encoding the prefix in the type level, so that if you accidentally pass in an invalid prefix, the code won’t compile, avoiding the need for runtime checks;
- Support TypeID with other UUID versions. Currently v7 (default), v1, v4, and v5 are supported.
Quick start
{-# LANGUAGE OverloadedStrings #-}
import Control.Exception
import Data.TypeID (TypeID)
import qualified Data.TypeID as TID
main :: IO ()
main = do
-- Make a TypeID with prefix 'mmzk':
typeID <- TID.genTypeID "mmzk"
putStrLn $ TID.toString typeID
-- Get components from the TypeID:
let prefix = TID.getPrefix typeID -- "mmzk"
uuid = TID.getUUID typeID
time = TID.getTime typeID -- A 'Word64' representing the timestamp in milliseconds
-- Make a TypeID without prefix:
typeID' <- TID.genTypeID ""
print typeID'
-- Make 10 TypeIDs in a batch. They are guaranteed to have the same timestamp and of ascending order:
typeIDs <- TID.genTypeIDs "mmzk" 10
mapM_ print typeIDs
-- Parse a TypeID from string:
case TID.parseString "mmzk_01h455vb4pex5vsknk084sn02q" of
Left err -> throwIO err
Right typeID -> print typeID
For a full list of functions on TypeID, see Data.TypeID.
More Usages
TypeID with other UUID Versions
We also support TypeID using some other versions of UUID, including v1, v4 and v5, which loses the monoticity property. To use it, simply import Data.TypeID.V4 instead of Data.TypeID. The following is an example using v4:
{-# LANGUAGE OverloadedStrings #-}
import Control.Exception
import Data.TypeID.V4 (TypeIDV4)
import qualified Data.TypeID.V4 as TID
main :: IO ()
main = do
-- Make a TypeID with prefix 'mmzk':
typeID <- TID.genTypeID "mmzk"
putStrLn $ TID.toString typeID
-- Get components from the TypeID:
let prefix = TID.getPrefix typeID -- "mmzk"
uuid = TID.getUUID typeID
-- Make a TypeID without prefix:
typeID' <- TID.genTypeID ""
print typeID'
-- Parse a TypeID from string:
case TID.parseString "mmzk_5hjpeh96458fct8t49fnf9farw" of
Left err -> throwIO err
Right typeID -> print typeID
Type-level TypeID (KindID)
When using TypeID, if we want to check if the type matches, we usually need to get the prefix of the TypeID and compare it with the desired prefix at runtime. However, with Haskell’s type system, we can do this at compile time instead. We call this TypeID with compile-time prefix a KindID.
Of course, that would require the desired prefix to be known at compile time. This is actually quite common, especially when we are using one prefix for one table in the database.
For example, suppose we have a function that takes a KindID with the prefix “user”, it may have a signature like this: f :: KindID "user" -> IO ().
Then if we try to pass in a KindID with the prefix “post”, the compiler will complain, thus removing the runtime check and the associated overhead.
All the prefixes are type-checked at compile time, so if we try to pass in invalid prefixes, the compiler (again) will complain.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
import Control.Exception
import Data.KindID (KindID)
import qualified Data.KindID as KID
main :: IO ()
main = do
-- Make a KindID with prefix 'mmzk':
kindID <- KID.genKindID @"mmzk" -- Has type `KindID "mmzk"`
putStrLn $ KID.toString kindID
-- Get components from the KindID:
let prefix = KID.getPrefix kindID -- "mmzk"
uuid = KID.getUUID kindID
time = KID.getTime kindID -- A 'Word64' representing the timestamp in milliseconds
-- Make a KindID without prefix:
kindID' <- KID.genKindID @"" -- Has type `KindID ""`
print kindID'
-- Make 10 KindIDs in a batch. They are guaranteed to have the same timestamp and of ascending order:
kindIDs <- KID.genKindIDs @"mmzk" 10
mapM_ print kindIDs
-- Parse a KindID from string:
case KID.parseString @"mmzk" "mmzk_01h455vb4pex5vsknk084sn02q" of
Left err -> throwIO err
Right kindID -> print kindID
For a full list of functions on KindID, see Data.KindID.
Functions with More General Types
TypeID and KindID shares many functions with the same name and functionality. So far, we are using qualified imports to diffentiate them (e.g KID.fromString and TID.fromString). Alternatively, we can use the methods of IDConv to use the same functions for both TypeID and KindID.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplication #-}
import Control.Exception
import Data.KindID
import Data.TypeID
main :: IO ()
main = do
-- Make a TypeID with prefix 'mmzk':
typeID <- genID @TypeID "mmzk"
print typeID
-- Make a KindID with prefix 'mmzk':
kindID <- genID @(KindID "mmzk")
print kindID
-- Parse a TypeID from string:
case string2ID "mmzk_01h455vb4pex5vsknk084sn02q" :: Maybe TypeID of
Left err -> throwIO err
Right typeID -> print typeID
-- Parse a KindID from string:
case string2ID "mmzk_01h455vb4pex5vsknk084sn02q" :: Maybe (KindID "mmzk") of
Left err -> throwIO err
Right kindID -> print kindID
-- Parse a KindID from string (wrong prefix):
case string2ID "mmzk_01h455vb4pex5vsknk084sn02q" :: Maybe (KindID "foo") of
Left err -> throwIO err -- Will throw here as the prefix matches not
Right kindID -> print kindID
We no longer need to use qualified imports, but on the down side, we need to add explicit type annotations. Therefore it is a matter of preference.
Note that with the class methods, the type application with Symbol no longer works as the full type must be provided. For example, string2ID @"mmzk" "mmzk_01h455vb4pex5vsknk084sn02q" will not compile.
For a full list of these functions, see Data.TypeID.Class.
KindID with Data Kinds
Instead of using raw Symbols as KindID prefixes, we can also define our custom data type for better semantics.
For example, suppose we have three tables for users, posts, and comments, and each table has a unique prefix, we can design the structure as following:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeApplications #-}
import Data.KindID
import Data.KindID.Class
data Prefix = User | Post | Comment
instance ToPrefix 'User where
type PrefixSymbol 'User = "user"
instance ToPrefix 'Post where
type PrefixSymbol 'Post = "post"
instance ToPrefix 'Comment where
type PrefixSymbol 'Comment = "comment"
Now we can use Prefix as a prefix for KindIDs, e.g.
main :: IO ()
main = do
-- ...
userID <- genKindID @'User -- Same as genKindID @"user"
postID <- genKindID @'Post -- Same as genKindID @"post"
commentID <- genKindID @'Comment -- Same as genKindID @"comment"
-- ...
For more information, see Data.KindID.Class.
Note
Functions not explicitly exported are considered internal and are subjected to changes.
Changes
Revision history for mmzk-typeid
0.7.1.1 – 2026-05-13
-
Support
random1.3. -
Tested with GHC 9.14.1.
-
Kindly contributed by @ysangkok.
0.7.1.0 – 2026-03-18
-
Add custom timestamp support for V7 UUID/TypeID/KindID generation.
- New functions
genUUIDWithTime,genUUIDWithTime', andgenUUIDsWithTimeinData.UUID.V7. - New functions
genTypeIDWithTime,genTypeIDWithTime', andgenTypeIDsWithTimeinData.TypeID.V7(and re-exported fromData.TypeID). - New unsafe variants
unsafeGenTypeIDWithTime,unsafeGenTypeIDWithTime', andunsafeGenTypeIDsWithTimeinData.TypeID.V7.Unsafe(and re-exported fromData.TypeID.Unsafe). - New functions
genKindIDWithTime,genKindIDWithTime', andgenKindIDsWithTimeinData.KindID.V7(and re-exported fromData.KindID). - These functions accept a
Word64timestamp (milliseconds since Unix epoch) and do not interact with the global monotonic state.
- New functions
-
Drop redundant
Typeablederivation. -
More tests.
-
Kindly contributed by @johnhampton.
0.7.0.2 – 2025-04-22
-
Fix a Haddock error on
ValidPrefixrelated to conditional compilation. -
Add more tests.
-
CI test on the latest GHC version.
0.7.0.1 – 2024-10-10
-
Add
Genericderivation toTypeID',KindID', andUUIDVersion. -
Test the type-level constraints of
KindIDusing an external GHC interpreter. -
Stop testing on GHC 9.2, replacing with tests on GHC 9.4.
-
Update description in “Data.UUID.V7” to reflect the fact that UUIDv7 standard has been finalised.
0.7.0.0 – 2024-07-03
-
Use
Strings instead ofTexts inside the constructors ofTypeIDErrorso that it is easier to use the promoted constructors. -
More complete compile-time error messages for
KindIDerrors. -
Hide away internal type-level programming details from Haddock.
- Many of the type-level helper declarations are now internal.
0.6.3.1 – 2024-06-23
- Relax the version constraint on
bytestringdependency.- Kindly contributed by @jflanglois.
0.6.3.0 – 2024-06-01
-
Update implementation so that the prefix for
KindIDnow conforms with specification v0.3.0. -
More useful compile-time errors for invalid
KindIDprefixes. -
TypeIDErrornow includes the contextual prefix when necessary to produce better error messages. -
More tests.
-
Fix known documentation typos.
0.6.2.0 – 2024-05-28
-
Fix the bug where the first 32768
TypeIDs may not of the same timestamp. -
Test on GHC 9.8.2.
0.6.0.1 – 2024-04-26
-
Fix typo in the maintainer’s email address.
- Astounded at the fact that I mismatched the local part and domain name and didn’t realise it for a year.
-
More test cases on parsing.
-
Fix other typos and inconsistencies in the documentation.
0.6.0.0 – 2024-04-19
-
Update implementation to conform with specification v0.3.0.
- Allow
TypeIDprefix to contain underscores. - Update parsing logic as well as
BinaryandStorableinstances to reflect the changes. - Add tests.
- Allow
-
Have a breaking change in
TypeIDErrorto better reflect the error cases for the update in the specification.
0.5.0.2 – 2024-3-10
-
Add
TypeableandDatainstances forTypeIDandKindID.- Kindly contributed by @shinzui.
-
Fix all warnings.
0.5.0.1 – 2023-9-18
- Fix bad links in the documentation.
0.5.0.0 – 2023-08-31
-
Support
TypeIDandKindIDwithUUIDsuffixes of version 5.- They are exported in
Data.TypeID.V5andData.KindID.V5.
- They are exported in
-
Tests for V5
TypeIDandKindID. -
Change signature for
genID_to supportUUIDv5. -
Decide against moving the
decoratemethod.
0.4.0.1 – 2023-08-19
-
Support
TypeIDandKindIDwithUUIDsuffixes of version 1.- They are exported in
Data.TypeID.V1andData.KindID.V1.
- They are exported in
-
Tests for V1
TypeIDandKindID. -
Fix documentation typos.
-
The
decoratemethod will be moved fromIDGentoIDTypein the next major release. -
The type signature for
genID_is likely to change in the next major release to supportUUIDv5. Hopefully it will not affect any existing concrete functions.
0.4.0.0 – 2023-08-08
-
Support
TypeIDandKindIDwithUUIDsuffixes of version 4.- They are exported in
Data.TypeID.V4andData.KindID.V4. - By default,
TypeIDandKindIDhas aUUIDsuffix of version 7. - The default
TypeIDandKindIDis also exported viaData.TypeID.V7andData.KindID.V7. - The constructor shapes have been changed, but it should not cause any problems since they are not exported.
- They are exported in
-
Remove deprecated
nilfunctions. -
Provide some default implementations for methods of
IDConv. -
Fix typoes in the Haddock.
-
Tests for V4
TypeIDandKindID.
0.3.1.0 – 2023-07-23
-
Add
parseStringM,parseTextM, andparseByteStringMtoIDConv.- Instead of returning an
Either, they throw an exception when the input is invalid.
- Instead of returning an
-
Add unsafe methods to
IDConv. -
Implement
StorableandBinaryinstances forTypeIDandKindID.- These instances are experimental since the specification does not propose any serialisation format.
0.3.0.1 – 2023-07-18
-
Add a version upper-bound for ‘uuid-types’.
-
Fix documentation typos.
0.3.0.0 – 2023-07-17
-
Use ‘uuid-types’ package’s
UUIDinstead of a custom type.Data.UUID.V7only retains the generation functions.- Other modules are not affected by this change.
-
Add
ReadandHashableinstances forTypeIDandKindID. -
Move
ValidPrefixandToPrefixtoData.KindID.Classmodule.- They are no longer exported from
Data.KindID.
- They are no longer exported from
-
Remove deprecated functions
unUUID,parseStringWithPrefix,parseTextWithPrefix,parseByteStringWithPrefix,nil, anddecorate. -
Re-implement
Showinstances forTypeIDandKindIDusing pretty-printtoString. -
Implement
TypeIDgeneration based on statelessUUIDv7.- It is faster but does not guarantee monotonicity if multiple processes are
generating
TypeIDs at the same time.
- It is faster but does not guarantee monotonicity if multiple processes are
generating
-
Introduce unsafe
TypeIDandKindIDfunctions for parsing and generating. They do not check the validity of the input and only behave well when the input is guaranteed to be valid. -
Add validity check on
TypeIDandKindIDgeneration.checkIDchecks the prefix and theUUID’s version and variant.checkIDWithEnvalso checks that theUUIDis generated in the past.
-
Deprecate
nilTypeIDandnilKindIDsince they are not useful. -
Remove dependency on ‘transformers’.
-
Fix typos in the documentation.
-
Update README examples.
-
More tests.
0.2.0.0 – 2023-07-14
-
Implement
KindIDto take arbitrary prefix type.- It can be a
Symbolas before, but it can also be any type that implementsToPrefixwhich dictates how to translate the prefix type to aSymbol.
- It can be a
-
Fix orphan instances for
TypeIDandKindID. -
Add
FromJSONKeyandToJSONKeyinstances forTypeIDandKindID. -
Introduce
IDTypeclass to unify thegetPrefix,getUUID, andgetTimefunctions ofTypeIDandKindID. -
Introduce
IDConvclass to unify the various conversion functions betweenTypeID/KindIDandString/Text/ByteString.- The original concrete functions remain, and the class is provided as an alternative.
-
Make the generation functions work with any
MonadIOthan justIO. -
Introduct
IDGenclass to unify the generation functions forTypeIDandKindID.- The original concrete functions remain, and the class is provided as an alternative.
-
Deprecate
unUUID,parseStringWithPrefix,parseTextWithPrefix,parseByteStringWithPrefix,nil, anddecorate. They are either replaced by functions of other names or are no longer necessary.- They will be removed in the next major version.
-
The
UUIDtype is expected to be removed in the next major version in favour of the type from the ‘uuid-types’ package. -
More tests.
0.1.0.0 – 2023-07-11
-
First version. Released on an unsuspecting world.
-
Implement
TypeIDas specified at https://github.com/jetpack-io/typeid. -
Add unit tests.
-
Add type-level
TypeIDprefixes. -
Add
FromJSONandToJSONinstances forTypeIDandKindID.