diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,76 @@
+# Changelog
+
+## v2.1.1.0 - "Generically Primitive" (2026-04-24)
+
+Tested on GHC 9.14.1.
+
+- Derive `Generic` for the exposed data types in `Data.Text.AhoCorasick.*`,
+  `Data.Text.BoyerMoore.*`, `Data.Text.BoyerMooreCI.*` and
+  `Data.Text.CaseSensitivity` ([#65](https://github.com/channable/alfred-margaret/pull/65)).
+- Replace the internal `Data.TypedByteArray` wrapper with `PrimArray` from
+  `Data.Primitive`; introduces the new internal module `Data.Primitive.Extended`.
+- Widen `containers` upper bound to include `0.8`.
+- Toolchain: switch development environment and CI to GHC 9.14 and LLVM 19;
+  add `direnv` support; bump pinned `nixpkgs` and Nix to 2.24.12.
+- Update `tested-with` to GHC 9.14.1.
+
+## v2.1.0.2 - "All the bounds!" (2024-09-06)
+
+Publishing requires `cabal` to know the version bounds as well.
+
+## v2.1.0.1 - "Higher bounds!" (2024-09-05)
+
+Tested on GHC 9.6.6.
+
+- Revise dependency bounds ([#62](https://github.com/channable/alfred-margaret/issues/62) thanks @Bodigrim)
+- Allow using primitive < 0.9 and vector < 0.14 ([#59](https://github.com/channable/alfred-margaret/pull/59) thanks @rampion)
+
+## v2.1.0.0 - "All The Cases!" (2022-08-31)
+
+- Added a case-insensitive variant of the Boyer-Moore algorithm in the `Data.Text.BoyerMooreCI.*` modules. ([#47](https://github.com/channable/alfred-margaret/pull/47))
+- Fixed a bug in the case-insensitive Aho-Corasick replacer where it would
+  replace the wrong section of the haystack when the needle had a different
+  byte-length than the matching part of the haystack. ([#47](https://github.com/channable/alfred-margaret/pull/47))
+- Allow mapping the payloads of Aho-Corasick automatons. ([#46](https://github.com/channable/alfred-margaret/pull/46))
+
+## v2.0.0.0 - "So Long Surrogates" (2022-05-02)
+
+Switched to text-2.0 which uses UTF-8 encoding internally.
+
+- Removed `Data.Text.Utf8.*` modules
+- Replaced `Data.Text.AhoCorasick.*` and `Data.Text.BoyerMoore.*` (previously using UTF-16) with the UTF-8 implementation
+
+## v1.1.2.0 - "ByteArray Boogaloo" (2022-04-21)
+
+Added UTF-8 implementations on a mock `Text` type (in `Data.Text.Utf8`).
+
+- Added `Data.Text.Utf8*` modules
+- Moved `CaseSensitivity` to its own `Data.Text.CaseSensitivity` module.
+- Added the private module `Data.TypedByteArray` which contains thin wrappers over `ByteArray` and `MutableByteArray`.
+- Replaced uses of `Data.Vector.Unboxed.Vector` by `TypedByteArray`.
+
+## v1.1.0.0 - "Moore Features" (2020-10-13)
+
+The most notable addition in this release is the implementation of the Boyer-Moore string search algorithm.
+
+**Compatibility:**
+
+- Extracted the UTF-16 manipulation functions from `Data.Text.AhoCorasick.Automaton` into `Data.Text.Utf16`
+- Changed `Data.Text.AhoCorasick.Searcher.Searcher` to remember the case sensitivity used for constructing the searcher
+- Removed `Data.Text.AhoCorasick.Searcher.containsAnyIgnoreCase`, the correct implementation is now chosen by `containsAny` based on the case sensitivity of the searcher
+
+Other changes:
+
+- Added `Data.Text.AhoCorasick.Splitter` for splitting a lot of text using the same needle
+- Added `Data.Text.BoyerMoore.Automaton`, a UTF-16 implementation of Boyer-Moore
+- Added `Data.Text.BoyerMoore.Searcher` for searching for multiple needles at once using Boyer-Moore
+- Added `Data.Text.BoyerMoore.Replacer` for replacing text based on the Boyer-Moore search
+- Added optional `FromJSON`/`ToJSON` instances for most types (can be toggled via `aeson` cabal flag)
+
+## v1.0.0.0 - "Initial Release" (2019-03-19)
+
+This is the initial open-source release.
+
+- Added `Data.Text.AhoCorasick.Automaton`, a UTF-16 implementation of the Aho-Corasick search algorithm
+- Added `Data.Text.AhoCorasick.Searcher`, a bulk search abstraction based on Aho-Corasick
+- Added `Data.Text.AhoCorasick.Replacer`, a bulk replace abstraction based on Aho-Corasick
diff --git a/alfred-margaret.cabal b/alfred-margaret.cabal
--- a/alfred-margaret.cabal
+++ b/alfred-margaret.cabal
@@ -1,5 +1,5 @@
 name:                alfred-margaret
-version:             2.1.0.2
+version:             2.1.1.0
 synopsis:            Fast Aho-Corasick string searching
 description:         An efficient implementation of the Aho-Corasick
                      string searching algorithm.
@@ -12,6 +12,7 @@
 category:            Data, Text
 build-type:          Simple
 extra-source-files:  README.md
+                   , CHANGELOG.md
                    , performance.png
 cabal-version:       >=1.10
 tested-with:
@@ -23,6 +24,8 @@
                    , GHC == 9.0.2
                      -- Nixpkgs unstable (Updated 2024-09-05)
                    , GHC == 9.6.6
+                     -- Nixpkgs unstable (Updated 2026-04-22)
+                   , GHC == 9.14.1
 
 source-repository head
   type:     git
@@ -57,10 +60,10 @@
                      , Data.Text.BoyerMooreCI.Automaton
                      , Data.Text.BoyerMooreCI.Replacer
                      , Data.Text.BoyerMooreCI.Searcher
-                     , Data.TypedByteArray
+  other-modules:       Data.Primitive.Extended
   build-depends:
       base             >= 4.7 && < 5
-    , containers       >= 0.6 && < 0.8
+    , containers       >= 0.6 && < 0.9
     , deepseq          >= 1.4 && < 1.6
     , hashable         >= 1.4.0.2 && < 1.6
     , primitive        >= 0.6.4 && < 0.10
@@ -76,7 +79,7 @@
   cpp-options:         -DHAS_AESON
   }
   if flag(llvm) {
-  ghc-options:         -fllvm -optlo=-O3 -optlo=-tailcallelim
+  ghc-options:         -fllvm -optlo=-passes=default<O3>
   }
 
 test-suite test-suite
diff --git a/src/Data/Primitive/Extended.hs b/src/Data/Primitive/Extended.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Primitive/Extended.hs
@@ -0,0 +1,21 @@
+-- | "Data.Primitive" extended with extra definitions.
+--
+-- Based on the ".Extended Modules" pattern:
+-- https://jaspervdj.be/posts/2015-01-20-haskell-design-patterns-extended-modules.html
+module Data.Primitive.Extended
+  ( module Data.Primitive
+  , replicateMutablePrimArray
+  )
+  where
+
+import Data.Primitive
+import Control.Monad.Primitive (PrimMonad(..))
+
+-- | Like 'replicatePrimArray', but does not freeze the array afterwards and
+-- stays within a monadic context, so it can easily be mutated further.
+{-# INLINE replicateMutablePrimArray #-}
+replicateMutablePrimArray :: (Prim a, PrimMonad m) => Int -> a -> m (MutablePrimArray (PrimState m) a)
+replicateMutablePrimArray len value = do
+    arr <- newPrimArray len
+    setPrimArray arr 0 len value
+    pure arr
diff --git a/src/Data/Text/AhoCorasick/Automaton.hs b/src/Data/Text/AhoCorasick/Automaton.hs
--- a/src/Data/Text/AhoCorasick/Automaton.hs
+++ b/src/Data/Text/AhoCorasick/Automaton.hs
@@ -46,8 +46,14 @@
 import Control.DeepSeq (NFData)
 import Data.Bits (Bits (shiftL, shiftR, (.&.), (.|.)))
 import Data.Char (chr)
-import Data.Foldable (foldl')
 import Data.IntMap.Strict (IntMap)
+import Data.Primitive.Extended
+  ( Prim
+  , PrimArray
+  , generatePrimArray
+  , indexPrimArray
+  , primArrayFromList
+  )
 import Data.Word (Word32, Word64)
 import GHC.Generics (Generic)
 
@@ -58,11 +64,9 @@
 
 import Data.Text.CaseSensitivity (CaseSensitivity (..))
 import Data.Text.Utf8 (CodePoint, CodeUnitIndex (CodeUnitIndex), Text (..))
-import Data.TypedByteArray (Prim, TypedByteArray)
 
 import qualified Data.Text as Text
 import qualified Data.Text.Utf8 as Utf8
-import qualified Data.TypedByteArray as TBA
 
 -- TYPES
 -- | A numbered state in the Aho-Corasick automaton.
@@ -105,14 +109,14 @@
   { machineValues               :: !(Vector.Vector [v])
   -- ^ For every state, the values associated with its needles. If the state is
   -- not a match state, the list is empty.
-  , machineTransitions          :: !(TypedByteArray Transition)
+  , machineTransitions          :: !(PrimArray Transition)
   -- ^ A packed vector of transitions. For every state, there is a slice of this
   -- vector that starts at the offset given by `machineOffsets`, and ends at the
   -- first wildcard transition.
-  , machineOffsets              :: !(TypedByteArray Offset)
+  , machineOffsets              :: !(PrimArray Offset)
   -- ^ For every state, the index into `machineTransitions` where the transition
   -- list for that state starts.
-  , machineRootAsciiTransitions :: !(TypedByteArray Transition)
+  , machineRootAsciiTransitions :: !(PrimArray Transition)
   -- ^ A lookup table for transitions from the root state, an optimization to
   -- avoid having to walk all transitions, at the cost of using a bit of
   -- additional memory.
@@ -159,11 +163,11 @@
 -- the transitions for a specific state, we also produce a vector of start
 -- indices. All transition lists are terminated by a wildcard transition, so
 -- there is no need to record the length.
-packTransitions :: [[Transition]] -> (TypedByteArray Transition, TypedByteArray Offset)
+packTransitions :: [[Transition]] -> (PrimArray Transition, PrimArray Offset)
 packTransitions transitions =
   let
-    packed = TBA.fromList $ concat transitions
-    offsets = TBA.fromList $ map fromIntegral $ scanl (+) 0 $ fmap List.length transitions
+    packed = primArrayFromList $ concat transitions
+    offsets = primArrayFromList $ map fromIntegral $ scanl (+) 0 $ fmap List.length transitions
   in
     (packed, offsets)
 
@@ -295,8 +299,8 @@
 -- O(1) lookup of a transition, rather than doing a linear scan over all
 -- transitions. The fallback goes back to the initial state, state 0.
 {-# NOINLINE buildAsciiTransitionLookupTable  #-}
-buildAsciiTransitionLookupTable :: IntMap State -> TypedByteArray Transition
-buildAsciiTransitionLookupTable transitions = TBA.generate asciiCount $ \i ->
+buildAsciiTransitionLookupTable :: IntMap State -> PrimArray Transition
+buildAsciiTransitionLookupTable transitions = generatePrimArray asciiCount $ \i ->
   case IntMap.lookup i transitions of
     Just state -> newTransition (Char.chr i) state
     Nothing    -> newWildcardTransition 0
@@ -384,8 +388,8 @@
 at = Vector.unsafeIndex
 
 {-# INLINE uAt #-}
-uAt :: Prim a => TypedByteArray a -> Int -> a
-uAt = TBA.unsafeIndex
+uAt :: Prim a => PrimArray a -> Int -> a
+uAt = indexPrimArray
 
 -- RUNNING THE MACHINE
 
@@ -560,5 +564,3 @@
   where
     loop "" = [""]
     loop (c:cs) = (:) <$> Utf8.unlowerCodePoint c <*> loop cs
-
-
diff --git a/src/Data/Text/AhoCorasick/Replacer.hs b/src/Data/Text/AhoCorasick/Replacer.hs
--- a/src/Data/Text/AhoCorasick/Replacer.hs
+++ b/src/Data/Text/AhoCorasick/Replacer.hs
@@ -156,7 +156,7 @@
 -- A match collected while running replacements. It is isomorphic to the Match
 -- reported by the automaton, but the data is arranged in a more useful way:
 -- as the start index and length of the match, and the replacement.
-data Match = Match !CodeUnitIndex !CodeUnitIndex !Text deriving (Eq, Ord, Show)
+data Match = Match !CodeUnitIndex !CodeUnitIndex !Text deriving (Eq, Ord, Show, Generic)
 
 -- | Apply replacements of all matches. Assumes that the matches are ordered by
 -- match position, and that no matches overlap.
diff --git a/src/Data/Text/AhoCorasick/Splitter.hs b/src/Data/Text/AhoCorasick/Splitter.hs
--- a/src/Data/Text/AhoCorasick/Splitter.hs
+++ b/src/Data/Text/AhoCorasick/Splitter.hs
@@ -4,6 +4,8 @@
 -- Licensed under the 3-clause BSD license, see the LICENSE file in the
 -- repository root.
 
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE CPP #-}
 
@@ -19,6 +21,7 @@
     , splitReverseIgnoreCase
     ) where
 
+import GHC.Generics (Generic)
 import Control.DeepSeq (NFData (..))
 import Data.Function (on)
 import Data.Hashable (Hashable (..))
@@ -46,6 +49,7 @@
     { splitterAutomaton :: AcMachine () -- INVARIANT: Exactly one needle.
     , splitterSeparator :: Text         -- INVARIANT: Equivalent to needle.
     }
+  deriving Generic
 
 #if defined(HAS_AESON)
 instance AE.ToJSON Splitter where
@@ -129,6 +133,7 @@
     , accumFragmentStart :: !Aho.CodeUnitIndex
       -- ^ First byte of current fragment (that is the non-separator part)
     }
+  deriving Generic
 
 -- | Finalizing the accumulator does more than just 'accumResult', hence this
 -- is a separate function.
diff --git a/src/Data/Text/BoyerMoore/Automaton.hs b/src/Data/Text/BoyerMoore/Automaton.hs
--- a/src/Data/Text/BoyerMoore/Automaton.hs
+++ b/src/Data/Text/BoyerMoore/Automaton.hs
@@ -37,6 +37,15 @@
 import Control.Monad (when)
 import Control.Monad.ST (runST)
 import Data.Hashable (Hashable (..))
+import Data.Primitive.Extended
+  ( Prim
+  , PrimArray
+  , indexPrimArray
+  , newPrimArray
+  , replicateMutablePrimArray
+  , unsafeFreezePrimArray
+  , writePrimArray
+  )
 import GHC.Generics (Generic)
 
 #if defined(HAS_AESON)
@@ -45,10 +54,8 @@
 
 import Data.Text.CaseSensitivity (CaseSensitivity (..))
 import Data.Text.Utf8 (CodeUnit, CodeUnitIndex (..), Text)
-import Data.TypedByteArray (Prim, TypedByteArray)
 
 import qualified Data.Text.Utf8 as Utf8
-import qualified Data.TypedByteArray as TBA
 
 data Next a
   = Done !a
@@ -167,7 +174,7 @@
 
 -- | The suffix table tells us for each character of the pattern how many characters we can
 -- jump ahead if the match fails at that point.
-newtype SuffixTable = SuffixTable (TypedByteArray Int)
+newtype SuffixTable = SuffixTable (PrimArray Int)
   deriving stock (Generic, Show)
   deriving anyclass (NFData)
 
@@ -180,7 +187,7 @@
 buildSuffixTable pattern = runST $ do
   let patLen = Utf8.lengthUtf8 pattern
 
-  table <- TBA.newTypedByteArray $ codeUnitIndex patLen
+  table <- newPrimArray $ codeUnitIndex patLen
 
   let
     -- Case 1: For each position of the pattern we record the shift that would align the pattern so
@@ -206,7 +213,7 @@
           prefixIndex
             | isPrefix pattern (p + 1) = p + 1
             | otherwise = lastPrefixIndex
-        TBA.writeTypedByteArray table (codeUnitIndex p) (codeUnitIndex $ prefixIndex + patLen - 1 - p)
+        writePrimArray table (codeUnitIndex p) (codeUnitIndex $ prefixIndex + patLen - 1 - p)
         init1 prefixIndex (p - 1)
       | otherwise = pure ()
 
@@ -219,21 +226,21 @@
         let
           suffixLen = suffixLength pattern p
         when (Utf8.unsafeIndexCodeUnit pattern (p - suffixLen) /= Utf8.unsafeIndexCodeUnit pattern (patLen - 1 - suffixLen)) $
-          TBA.writeTypedByteArray table (codeUnitIndex $ patLen - 1 - suffixLen) (codeUnitIndex $ patLen - 1 - p + suffixLen)
+          writePrimArray table (codeUnitIndex $ patLen - 1 - suffixLen) (codeUnitIndex $ patLen - 1 - p + suffixLen)
         init2 (p + 1)
       | otherwise = pure ()
 
   init1 (patLen - 1) (patLen - 1)
   init2 0
 
-  SuffixTable <$> TBA.unsafeFreezeTypedByteArray table
+  SuffixTable <$> unsafeFreezePrimArray table
 
 
 -- | The bad char table tells us how far we may skip ahead when encountering a certain character
 -- in the input string. For example, if there's a character that is not contained in the pattern at
 -- all, we can skip ahead until after that character.
 data BadCharTable = BadCharTable
-  { badCharTableEntries :: {-# UNPACK #-} !(TypedByteArray Int)
+  { badCharTableEntries :: {-# UNPACK #-} !(PrimArray Int)
     -- ^ The element type should be CodeUnitIndex, but there's no unboxed vector for that type, and
     -- defining it would be a lot of boilerplate.
   , badCharTablePatternLen :: CodeUnitIndex
@@ -284,7 +291,7 @@
 
   -- Initialize table with the maximum skip distance, which is the length of the pattern.
   -- This applies to all characters that are not part of the pattern.
-  asciiTable <- TBA.replicate badcharTableSize $ codeUnitIndex patLen
+  asciiTable <- replicateMutablePrimArray badcharTableSize $ codeUnitIndex patLen
 
   let
     -- Fill the bad character table based on the rightmost occurrence of a character in the pattern.
@@ -319,13 +326,13 @@
       -- for(i = 0; i < patLen - 1; i++) {
       | i < patLen - 1 = do
         let patChar = Utf8.unsafeIndexCodeUnit pattern i
-        TBA.writeTypedByteArray asciiTable (fromIntegral patChar) (codeUnitIndex $ patLen - 1 - i)
+        writePrimArray asciiTable (fromIntegral patChar) (codeUnitIndex $ patLen - 1 - i)
         fillTable (i + 1)
       | otherwise = pure ()
 
   fillTable 0
 
-  asciiTableFrozen <- TBA.unsafeFreezeTypedByteArray asciiTable
+  asciiTableFrozen <- unsafeFreezePrimArray asciiTable
 
   pure BadCharTable
     { badCharTableEntries = asciiTableFrozen
@@ -336,6 +343,6 @@
 -- Helper functions for easily toggling the safety of this module
 
 -- | Read from a lookup table at the specified index.
-indexTable :: Prim a => TypedByteArray a -> Int -> a
+indexTable :: Prim a => PrimArray a -> Int -> a
 {-# INLINE indexTable #-}
-indexTable = TBA.unsafeIndex
+indexTable = indexPrimArray
diff --git a/src/Data/Text/BoyerMoore/Replacer.hs b/src/Data/Text/BoyerMoore/Replacer.hs
--- a/src/Data/Text/BoyerMoore/Replacer.hs
+++ b/src/Data/Text/BoyerMoore/Replacer.hs
@@ -4,6 +4,8 @@
 -- Licensed under the 3-clause BSD license, see the LICENSE file in the
 -- repository root.
 
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
 {-# LANGUAGE FlexibleInstances #-}
 
@@ -12,6 +14,7 @@
       replaceSingleLimited
     ) where
 
+import GHC.Generics (Generic)
 import Data.Text.Utf8 (Text)
 import Data.Text.BoyerMoore.Automaton (Automaton, CodeUnitIndex)
 
@@ -89,3 +92,4 @@
   , rsLength :: !CodeUnitIndex
     -- ^ Length of the newly build string so far, measured in CodeUnits
   }
+  deriving Generic
diff --git a/src/Data/Text/BoyerMooreCI/Automaton.hs b/src/Data/Text/BoyerMooreCI/Automaton.hs
--- a/src/Data/Text/BoyerMooreCI/Automaton.hs
+++ b/src/Data/Text/BoyerMooreCI/Automaton.hs
@@ -37,6 +37,19 @@
 import Control.DeepSeq (NFData)
 import Control.Monad.ST (runST)
 import Data.Hashable (Hashable (..))
+import Data.Primitive.Extended
+  ( Prim
+  , PrimArray
+  , foldrPrimArray
+  , indexPrimArray
+  , newPrimArray
+  , primArrayFromList
+  , primArrayToList
+  , replicateMutablePrimArray
+  , sizeofPrimArray
+  , unsafeFreezePrimArray
+  , writePrimArray
+  )
 import Data.Text.Internal (Text (..))
 import GHC.Generics (Generic)
 
@@ -46,13 +59,11 @@
 
 import Data.Text.CaseSensitivity (CaseSensitivity (..))
 import Data.Text.Utf8 (BackwardsIter (..), CodePoint, CodeUnitIndex (..))
-import Data.TypedByteArray (Prim, TypedByteArray)
 
 import qualified Data.Char as Char
 import qualified Data.HashMap.Strict as HashMap
 import qualified Data.Text as Text
 import qualified Data.Text.Utf8 as Utf8
-import qualified Data.TypedByteArray as TBA
 
 data Next a
   = Done !a
@@ -70,7 +81,7 @@
 -- finding @aaaa@ in @aaaaa....aaaaaa@ as for each match it would scan back the whole /m/ characters
 -- of the pattern.
 data Automaton = Automaton
-  { automatonPattern :: !(TypedByteArray CodePoint)
+  { automatonPattern :: !(PrimArray CodePoint)
   , automatonPatternHash :: !Int
   , automatonSuffixTable :: !SuffixTable
   , automatonBadCharLookup :: !BadCharLookup
@@ -103,7 +114,7 @@
     , automatonMinPatternSkip = minimumSkipForVector patternVec
     }
   where
-    patternVec = TBA.fromList (Text.unpack pattern_)
+    patternVec = primArrayFromList (Text.unpack pattern_)
 
 -- | Finds all matches in the text, calling the match callback with the first and last byte index of
 -- each match of the pattern.
@@ -115,7 +126,7 @@
   -> a
 {-# INLINE runText #-}
 runText seed f automaton !text
-  | TBA.null pattern_ = seed
+  | sizeofPrimArray pattern_ == 0 = seed
   | otherwise = alignPattern seed initialHaystackMin (initialHaystackMin + minPatternSkip - 1)
   where
     Automaton pattern_ _ suffixTable badCharTable minPatternSkip = automaton
@@ -142,7 +153,7 @@
       | otherwise =
           let
             !iter = Utf8.unsafeIndexAnywhereInCodePoint' (case text of Text d _ _ -> d) alignmentEnd
-            !patternIndex = TBA.length pattern_ - 1
+            !patternIndex = sizeofPrimArray pattern_ - 1
             -- End of char may be somewhere different than where we started looking
             !alignmentEnd' = backwardsIterEndOfChar iter
           in
@@ -160,7 +171,7 @@
       let
         !haystackCodePointLower = Utf8.lowerCodePoint (backwardsIterChar iter)
       in
-        case haystackCodePointLower == TBA.unsafeIndex pattern_ patternIndex of
+        case haystackCodePointLower == indexPrimArray pattern_ patternIndex of
 
           True | patternIndex == 0 ->
             -- We found a complete match (all pattern characters matched)
@@ -214,7 +225,7 @@
 
 -- | Return the pattern that was used to construct the automaton, O(n).
 patternText :: Automaton -> Text
-patternText = Text.pack . TBA.toList . automatonPattern
+patternText = Text.pack . primArrayToList . automatonPattern
 
 
 -- | Number of bytes that we can skip in the haystack if we want to skip no more
@@ -248,31 +259,31 @@
 --     minimumSkipForVector (TBA.fromList "ab..cd") == 6
 --     minimumSkipForVector (TBA.fromList "aⱥ💩") == 7
 --
-minimumSkipForVector :: TypedByteArray CodePoint -> CodeUnitIndex
-minimumSkipForVector = TBA.foldr (\cp s -> s + minimumSkipForCodePoint cp) 0
+minimumSkipForVector :: PrimArray CodePoint -> CodeUnitIndex
+minimumSkipForVector = foldrPrimArray (\cp s -> s + minimumSkipForCodePoint cp) 0
 
 
 -- | The suffix table tells us for each codepoint (not byte!) of the pattern how many bytes (not
 -- codepoints!) we can jump ahead if the match fails at that point.
-newtype SuffixTable = SuffixTable (TypedByteArray CodeUnitIndex)
+newtype SuffixTable = SuffixTable (PrimArray CodeUnitIndex)
   deriving stock (Generic)
   deriving anyclass (NFData)
 
 instance Show SuffixTable where
-  show (SuffixTable table) = "SuffixTable (TBA.toList " <> show (TBA.toList table) <> ")"
+  show (SuffixTable table) = "SuffixTable (TBA.toList " <> show (primArrayToList table) <> ")"
 
 -- | Lookup an entry in the suffix table.
 suffixLookup :: SuffixTable -> Int -> CodeUnitIndex
 {-# INLINE suffixLookup #-}
 suffixLookup (SuffixTable table) = indexTable table
 
-buildSuffixTable :: TypedByteArray CodePoint -> SuffixTable
+buildSuffixTable :: PrimArray CodePoint -> SuffixTable
 buildSuffixTable pattern_ = runST $ do
   let
-    patLen = TBA.length pattern_
+    patLen = sizeofPrimArray pattern_
     wholePatternSkip = minimumSkipForVector pattern_
 
-  table <- TBA.newTypedByteArray patLen
+  table <- newPrimArray patLen
 
   let
     -- Case 1: For each position of the pattern we record the shift that would align the pattern so
@@ -303,7 +314,7 @@
                         Nothing -> lastSkipBytes
                         -- Skip the whole pattern _except_ the bytes for the suffix(==prefix)
                         Just nonSkippableBytes -> wholePatternSkip - nonSkippableBytes
-        TBA.writeTypedByteArray table p skipBytes
+        writePrimArray table p skipBytes
         init1 skipBytes (p - 1)
       | otherwise = pure ()
 
@@ -314,30 +325,30 @@
     init2 p skipBytes
       | p < patLen - 1 = do
           -- If we find a suffix that ends at p, we can skip everything _after_ p.
-          let skipBytes' = skipBytes - minimumSkipForCodePoint (TBA.unsafeIndex pattern_ p)
+          let skipBytes' = skipBytes - minimumSkipForCodePoint (indexPrimArray pattern_ p)
           case substringIsSuffix pattern_ p of
             Nothing -> pure ()
             Just suffixLen -> do
-              TBA.writeTypedByteArray table (patLen - 1 - suffixLen) skipBytes'
+              writePrimArray table (patLen - 1 - suffixLen) skipBytes'
           init2 (p + 1) skipBytes'
       | otherwise = pure ()
 
   init1 (wholePatternSkip-1) (patLen - 1)
   init2 0 wholePatternSkip
-  TBA.writeTypedByteArray table (patLen - 1) (CodeUnitIndex 1)
+  writePrimArray table (patLen - 1) (CodeUnitIndex 1)
 
-  SuffixTable <$> TBA.unsafeFreezeTypedByteArray table
+  SuffixTable <$> unsafeFreezePrimArray table
 
 -- | True if the suffix of the @pattern@ starting from @pos@ is a prefix of the pattern
 -- For example, @suffixIsPrefix \"aabbaa\" 4 == Just 2@.
-suffixIsPrefix :: TypedByteArray CodePoint -> Int -> Maybe CodeUnitIndex
+suffixIsPrefix :: PrimArray CodePoint -> Int -> Maybe CodeUnitIndex
 suffixIsPrefix pattern_ pos = go 0 (CodeUnitIndex 0)
   where
-    suffixLen = TBA.length pattern_ - pos
+    suffixLen = sizeofPrimArray pattern_ - pos
     go !i !skipBytes
       | i < suffixLen =
-          let prefixChar = TBA.unsafeIndex pattern_ i in
-          if prefixChar == TBA.unsafeIndex pattern_ (pos + i)
+          let prefixChar = indexPrimArray pattern_ i in
+          if prefixChar == indexPrimArray pattern_ (pos + i)
             then go (i + 1) (skipBytes + minimumSkipForCodePoint prefixChar)
             else Nothing
       | otherwise = Just skipBytes
@@ -362,12 +373,12 @@
 --   substringIsSuffix (Vector.fromList "abaacaabcbaac") 4 == Just 4  -- baac == baac
 --   substringIsSuffix (Vector.fromList "abaacaabcbaac") 8 == Just 1  -- c == c
 --
-substringIsSuffix :: TypedByteArray CodePoint -> Int -> Maybe Int
+substringIsSuffix :: PrimArray CodePoint -> Int -> Maybe Int
 substringIsSuffix pattern_ pos = go 0
   where
-    patLen = TBA.length pattern_
+    patLen = sizeofPrimArray pattern_
     go i | i > pos = Nothing  -- prefix==suffix, so already covered by suffixIsPrefix
-         | TBA.unsafeIndex pattern_ (pos - i) == TBA.unsafeIndex pattern_ (patLen - 1 - i) =
+         | indexPrimArray pattern_ (pos - i) == indexPrimArray pattern_ (patLen - 1 - i) =
              go (i + 1)
          | i == 0 = Nothing  -- Nothing matched
          | otherwise = Just i
@@ -377,7 +388,7 @@
 -- character in the input string. For example, if there's a character that is not contained in the
 -- pattern at all, we can skip ahead until after that character.
 data BadCharLookup = BadCharLookup
-  { badCharLookupTable :: {-# UNPACK #-} !(TypedByteArray CodeUnitIndex)
+  { badCharLookupTable :: {-# UNPACK #-} !(PrimArray CodeUnitIndex)
   , badCharLookupMap :: !(HashMap.HashMap CodePoint CodeUnitIndex)
   , badCharLookupDefault :: !CodeUnitIndex
   }
@@ -400,7 +411,7 @@
 
 
 
-buildBadCharLookup :: TypedByteArray CodePoint -> BadCharLookup
+buildBadCharLookup :: PrimArray CodePoint -> BadCharLookup
 buildBadCharLookup pattern_ = runST $ do
 
   let
@@ -408,7 +419,7 @@
 
   -- Initialize table with the maximum skip distance, which is the length of the pattern.
   -- This applies to all characters that are not part of the pattern.
-  table <- (TBA.replicate badCharTableSize defaultSkip)
+  table <- replicateMutablePrimArray badCharTableSize defaultSkip
 
   let
     -- Fill the bad character table based on the rightmost occurrence of a character in the pattern.
@@ -449,15 +460,15 @@
         let skipBytes' = skipBytes - minimumSkipForCodePoint patChar in
         if fromEnum patChar < badCharTableSize
         then do
-          TBA.writeTypedByteArray table (fromEnum patChar) skipBytes'
+          writePrimArray table (fromEnum patChar) skipBytes'
           fillTable badCharMap skipBytes' patChars
         else
           let badCharMap' = HashMap.insert patChar skipBytes' badCharMap
           in fillTable badCharMap' skipBytes' patChars
 
-  badCharMap <- fillTable HashMap.empty defaultSkip (TBA.toList pattern_)
+  badCharMap <- fillTable HashMap.empty defaultSkip (primArrayToList pattern_)
 
-  tableFrozen <- TBA.unsafeFreezeTypedByteArray table
+  tableFrozen <- unsafeFreezePrimArray table
 
   pure BadCharLookup
     { badCharLookupTable = tableFrozen
@@ -469,6 +480,6 @@
 -- Helper functions for easily toggling the safety of this module
 
 -- | Read from a lookup table at the specified index.
-indexTable :: Prim a => TypedByteArray a -> Int -> a
+indexTable :: Prim a => PrimArray a -> Int -> a
 {-# INLINE indexTable #-}
-indexTable = TBA.unsafeIndex
+indexTable = indexPrimArray
diff --git a/src/Data/Text/BoyerMooreCI/Replacer.hs b/src/Data/Text/BoyerMooreCI/Replacer.hs
--- a/src/Data/Text/BoyerMooreCI/Replacer.hs
+++ b/src/Data/Text/BoyerMooreCI/Replacer.hs
@@ -4,6 +4,8 @@
 -- Licensed under the 3-clause BSD license, see the LICENSE file in the
 -- repository root.
 
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
 {-# LANGUAGE FlexibleInstances #-}
 
@@ -12,6 +14,7 @@
       replaceSingleLimited
     ) where
 
+import GHC.Generics (Generic)
 import Data.Text.Utf8 (Text)
 import Data.Text.BoyerMooreCI.Automaton (Automaton, CodeUnitIndex)
 
@@ -87,3 +90,4 @@
   , rsLength :: !CodeUnitIndex
     -- ^ Length of the newly build string so far, measured in CodeUnits
   }
+  deriving Generic
diff --git a/src/Data/Text/Utf8.hs b/src/Data/Text/Utf8.hs
--- a/src/Data/Text/Utf8.hs
+++ b/src/Data/Text/Utf8.hs
@@ -361,6 +361,7 @@
   , backwardsIterEndOfChar :: {-# UNPACK #-} !CodeUnitIndex
     -- ^ Points to the last byte of the codepoint that we're focused on
   }
+  deriving Generic
 
 -- | Similar to unsafeIndexCodePoint', but assumes that the given index is the
 -- end of a utf8 codepoint. It returns the decoded code point and the index
diff --git a/src/Data/TypedByteArray.hs b/src/Data/TypedByteArray.hs
deleted file mode 100644
--- a/src/Data/TypedByteArray.hs
+++ /dev/null
@@ -1,112 +0,0 @@
--- Alfred-Margaret: Fast Aho-Corasick string searching
--- Copyright 2022 Channable
---
--- Licensed under the 3-clause BSD license, see the LICENSE file in the
--- repository root.
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Data.TypedByteArray
-    ( Data.TypedByteArray.replicate
-    , MutableTypedByteArray
-    , Prim
-    , TypedByteArray
-    , fromList
-    , toList
-    , generate
-    , newTypedByteArray
-    , unsafeFreezeTypedByteArray
-    , unsafeIndex
-    , writeTypedByteArray
-    , null
-    , length
-    , foldr
-    ) where
-
-import Prelude hiding (foldr, length, null)
-
-import Control.DeepSeq (NFData (rnf))
-import Control.Monad.Primitive (PrimMonad (PrimState))
-import Control.Monad.ST (runST)
-import Data.Primitive (ByteArray (ByteArray), MutableByteArray, Prim, byteArrayFromList,
-                       indexByteArray, newByteArray, sizeOf, unsafeFreezeByteArray, writeByteArray)
-
-import qualified Data.Primitive as Primitive
-
-
--- | Thin wrapper around 'ByteArray' that makes signatures and indexing nicer to read.
-newtype TypedByteArray a = TypedByteArray ByteArray
-    deriving (Show, Eq)
-
--- | Thin wrapper around 'MutableByteArray s' that makes signatures and indexing nicer to read.
-newtype MutableTypedByteArray a s = MutableTypedByteArray (MutableByteArray s)
-
-instance NFData (TypedByteArray a) where
-    rnf (TypedByteArray (ByteArray !_)) = ()
-
-{-# INLINE newTypedByteArray #-}
-newTypedByteArray :: forall a m. (Prim a, PrimMonad m) => Int -> m (MutableTypedByteArray a (PrimState m))
-newTypedByteArray = fmap MutableTypedByteArray . newByteArray . (* sizeOf (undefined :: a))
-
-{-# INLINE fromList #-}
-fromList :: Prim a => [a] -> TypedByteArray a
-fromList = TypedByteArray . byteArrayFromList
-
-{-# INLINE toList #-}
-toList :: Prim a => TypedByteArray a -> [a]
-toList = foldr (:) []
-
--- | Element index without bounds checking.
-{-# INLINE unsafeIndex #-}
-unsafeIndex :: Prim a => TypedByteArray a -> Int -> a
-unsafeIndex (TypedByteArray arr) = indexByteArray arr
-
-{-# INLINE generate #-}
--- | Construct a 'TypedByteArray' of the given length by applying the function to each index in @[0..n-1]@.
-generate :: Prim a => Int -> (Int -> a) -> TypedByteArray a
-generate !n f = runST $ do
-    -- Allocate enough space for n elements of type a
-    arr <- newTypedByteArray n
-    intLoop 0 n $ \i -> i `seq` writeTypedByteArray arr i $ f i
-
-    unsafeFreezeTypedByteArray arr
-
-replicate :: (Prim a, PrimMonad m) => Int -> a -> m (MutableTypedByteArray a (PrimState m))
-replicate n value = do
-    arr <- newTypedByteArray n
-    intLoop 0 n $ \i -> i `seq` writeTypedByteArray arr i value
-    pure arr
-
-{-# INLINE writeTypedByteArray #-}
-writeTypedByteArray :: (Prim a, PrimMonad m) => MutableTypedByteArray a (PrimState m) -> Int -> a -> m ()
-writeTypedByteArray (MutableTypedByteArray array) = writeByteArray array
-
-{-# INLINE unsafeFreezeTypedByteArray #-}
-unsafeFreezeTypedByteArray :: PrimMonad m => MutableTypedByteArray a (PrimState m) -> m (TypedByteArray a)
-unsafeFreezeTypedByteArray (MutableTypedByteArray array) = TypedByteArray <$> unsafeFreezeByteArray array
-
-{-# INLINE intLoop #-}
-intLoop :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
-intLoop !iStart !n p = go iStart
-    where
-        go !i
-            | i >= n = pure ()
-            | otherwise = do
-                p i
-                go (i + 1)
-
-{-# INLINE null #-}
-null :: TypedByteArray a -> Bool
-null (TypedByteArray arr) =
-  Primitive.sizeofByteArray arr == 0  -- under the assumption that elements are not size 0
-
-{-# INLINE length #-}
-length :: forall a. Prim a => TypedByteArray a -> Int
-length (TypedByteArray arr) =
-  -- This is how foldrByteArray calculates it, so must be good
-  Primitive.sizeofByteArray arr `quot` sizeOf (undefined :: a)
-
-{-# INLINE foldr #-}
-foldr :: Prim a => (a -> b -> b) -> b -> TypedByteArray a -> b
-foldr f a (TypedByteArray arr) = Primitive.foldrByteArray f a arr
-
