diff --git a/dataframe-core.cabal b/dataframe-core.cabal
--- a/dataframe-core.cabal
+++ b/dataframe-core.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               dataframe-core
-version:            2.1.1.2
+version:            2.2.0.0
 synopsis:           Core data structures for the dataframe library.
 description:
     Minimal interchange-format types for the @dataframe@ ecosystem:
diff --git a/src/DataFrame/Typed/Generic.hs b/src/DataFrame/Typed/Generic.hs
--- a/src/DataFrame/Typed/Generic.hs
+++ b/src/DataFrame/Typed/Generic.hs
@@ -86,7 +86,6 @@
 import qualified DataFrame.Internal.DataFrame as D
 import DataFrame.Typed.Record (requireColumn)
 import DataFrame.Typed.Schema (Append)
-import DataFrame.Typed.Types (Column)
 import DataFrame.Typed.Util (camelToSnake)
 
 {- | Field-name policy applied to record selectors when computing
@@ -97,15 +96,15 @@
 -}
 data NameCase = SnakeCase | IdentityCase
 
-{- | The schema type @[Column name ty, ...]@ derived from the 'Rep' of a
+{- | The schema type @'[ '(name, ty), ...]@ derived from the 'Rep' of a
 record type, with the given 'NameCase' applied to each field name.
 -}
-type family RepToSchema (nc :: NameCase) (r :: Type -> Type) :: [Type] where
+type family RepToSchema (nc :: NameCase) (r :: Type -> Type) :: [(Symbol, Type)] where
     RepToSchema nc (M1 D _ f) = RepToSchema nc f
     RepToSchema nc (M1 C _ f) = RepToSchema nc f
     RepToSchema nc (a :*: b) = Append (RepToSchema nc a) (RepToSchema nc b)
     RepToSchema nc (M1 S ('MetaSel ('Just name) _ _ _) (K1 _ a)) =
-        '[Column (TransformName nc name) a]
+        '(TransformName nc name, a) ': '[]
 
 type family TransformName (nc :: NameCase) (name :: Symbol) :: Symbol where
     TransformName 'SnakeCase s = CamelToSnake s
diff --git a/src/DataFrame/Typed/Record.hs b/src/DataFrame/Typed/Record.hs
--- a/src/DataFrame/Typed/Record.hs
+++ b/src/DataFrame/Typed/Record.hs
@@ -38,6 +38,7 @@
 import DataFrame.Internal.DataFrame (fromNamedColumns)
 import qualified DataFrame.Internal.DataFrame as D
 import DataFrame.Typed.Types (TypedDataFrame (..))
+import GHC.TypeLits
 
 {- | Bridge a Haskell record type @a@ to a typed-dataframe schema.
 
@@ -51,7 +52,7 @@
 @Left err@ if a column is missing or has the wrong type.
 -}
 class HasSchema a where
-    type Schema a :: [Type]
+    type Schema a :: [(Symbol, Type)]
     toColumns :: [a] -> [(T.Text, C.Column)]
     fromColumns :: D.DataFrame -> Either T.Text [a]
 
diff --git a/src/DataFrame/Typed/Schema.hs b/src/DataFrame/Typed/Schema.hs
--- a/src/DataFrame/Typed/Schema.hs
+++ b/src/DataFrame/Typed/Schema.hs
@@ -79,12 +79,11 @@
 
 import DataFrame.Internal.Column (Columnable)
 import DataFrame.Internal.Types (These)
-import DataFrame.Typed.Types (Column)
 
 -- | Look up the element type of a column by name.
-type family Lookup (name :: Symbol) (cols :: [Type]) :: Type where
-    Lookup name (Column name a ': _) = a
-    Lookup name (Column _ _ ': rest) = Lookup name rest
+type family Lookup (name :: Symbol) (cols :: [(Symbol, Type)]) :: Type where
+    Lookup name ('(name, a) ': _) = a
+    Lookup name (_ ': rest) = Lookup name rest
     Lookup name '[] =
         TypeError
             ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")
@@ -93,22 +92,25 @@
 'TypeError' when the column is not found.  Use together with
 'AssertPresent' so the error fires exactly once.
 -}
-type family SafeLookup (name :: Symbol) (cols :: [Type]) :: Type where
-    SafeLookup name (Column name a ': _) = a
-    SafeLookup name (Column _ _ ': rest) = SafeLookup name rest
+type family SafeLookup (name :: Symbol) (cols :: [(Symbol, Type)]) :: Type where
+    SafeLookup name ('(name, a) ': _) = a
+    SafeLookup name (_ ': rest) = SafeLookup name rest
     SafeLookup name '[] = Int
 
 -- | Unwrap a Maybe from a type after we impute values.
-type family Impute (name :: Symbol) (cols :: [Type]) :: [Type] where
-    Impute name (Column name (Maybe a) ': rest) = Column name a ': rest
-    Impute name (Column name _ ': rest) =
+type family Impute (name :: Symbol) (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
+    Impute name ('(name, Maybe a) ': rest) = '(name, a) ': rest
+    Impute name ('(name, _) ': rest) =
         TypeError
             ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' is not of kind Maybe *")
     Impute name (col ': rest) = col ': Impute name rest
     Impute name '[] = '[]
 
-type family SetColumnType (name :: Symbol) (b :: Type) (cols :: [Type]) :: [Type] where
-    SetColumnType name b (Column name _ ': rest) = Column name b ': rest
+type family
+    SetColumnType (name :: Symbol) (b :: Type) (cols :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
+    SetColumnType name b ('(name, _) ': rest) = '(name, b) ': rest
     SetColumnType name b (col ': rest) = col ': SetColumnType name b rest
     SetColumnType name b '[] =
         TypeError
@@ -120,26 +122,26 @@
     Snoc (y ': ys) x = y ': Snoc ys x
 
 -- | Check whether a column name exists in a schema (type-level Bool).
-type family HasName (name :: Symbol) (cols :: [Type]) :: Bool where
-    HasName name (Column name _ ': _) = 'True
-    HasName name (Column _ _ ': rest) = HasName name rest
+type family HasName (name :: Symbol) (cols :: [(Symbol, Type)]) :: Bool where
+    HasName name ('(name, _) ': _) = 'True
+    HasName name (_ ': rest) = HasName name rest
     HasName name '[] = 'False
 
 -- | Remove a column by name from a schema.
-type family RemoveColumn (name :: Symbol) (cols :: [Type]) :: [Type] where
-    RemoveColumn name (Column name _ ': rest) = rest
+type family RemoveColumn (name :: Symbol) (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
+    RemoveColumn name ('(name, _) ': rest) = rest
     RemoveColumn name (col ': rest) = col ': RemoveColumn name rest
     RemoveColumn name '[] = '[]
 
 -- | Select a subset of columns by a list of names.
-type family SubsetSchema (names :: [Symbol]) (cols :: [Type]) :: [Type] where
+type family SubsetSchema (names :: [Symbol]) (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
     SubsetSchema '[] cols = '[]
-    SubsetSchema (n ': ns) cols = Column n (Lookup n cols) ': SubsetSchema ns cols
+    SubsetSchema (n ': ns) cols = '(n, Lookup n cols) ': SubsetSchema ns cols
 
 -- | Exclude columns by a list of names.
-type family ExcludeSchema (names :: [Symbol]) (cols :: [Type]) :: [Type] where
+type family ExcludeSchema (names :: [Symbol]) (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
     ExcludeSchema names '[] = '[]
-    ExcludeSchema names (Column n a ': rest) =
+    ExcludeSchema names ('(n, a) ': rest) =
         ExcludeSchemaHelper (IsElem n names) n a names rest
 
 type family
@@ -148,12 +150,12 @@
         (n :: Symbol)
         (a :: Type)
         (names :: [Symbol])
-        (rest :: [Type]) ::
-        [Type]
+        (rest :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
     where
     ExcludeSchemaHelper 'True n a names rest = ExcludeSchema names rest
     ExcludeSchemaHelper 'False n a names rest =
-        Column n a ': ExcludeSchema names rest
+        '(n, a) ': ExcludeSchema names rest
 
 -- | Type-level elem for Symbols
 type family IsElem (x :: Symbol) (xs :: [Symbol]) :: Bool where
@@ -162,15 +164,21 @@
     IsElem x (_ ': xs) = IsElem x xs
 
 -- | Rename a column in the schema.
-type family RenameInSchema (old :: Symbol) (new :: Symbol) (cols :: [Type]) :: [Type] where
-    RenameInSchema old new (Column old a ': rest) = Column new a ': rest
+type family
+    RenameInSchema (old :: Symbol) (new :: Symbol) (cols :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
+    RenameInSchema old new ('(old, a) ': rest) = '(new, a) ': rest
     RenameInSchema old new (col ': rest) = col ': RenameInSchema old new rest
     RenameInSchema old new '[] =
         TypeError
             ('Text "Cannot rename: column '" ':<>: 'Text old ':<>: 'Text "' not found")
 
 -- | Rename multiple columns.
-type family RenameManyInSchema (pairs :: [(Symbol, Symbol)]) (cols :: [Type]) :: [Type] where
+type family
+    RenameManyInSchema (pairs :: [(Symbol, Symbol)]) (cols :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     RenameManyInSchema '[] cols = cols
     RenameManyInSchema ('(old, new) ': rest) cols =
         RenameManyInSchema rest (RenameInSchema old new cols)
@@ -181,24 +189,27 @@
     Append (x ': xs) ys = x ': Append xs ys
 
 -- | Reverse a type-level list.
-type family Reverse (xs :: [Type]) :: [Type] where
+type family Reverse (xs :: [(Symbol, Type)]) :: [(Symbol, Type)] where
     Reverse xs = ReverseAcc xs '[]
 
-type family ReverseAcc (xs :: [Type]) (acc :: [Type]) :: [Type] where
+type family
+    ReverseAcc (xs :: [(Symbol, Type)]) (acc :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     ReverseAcc '[] acc = acc
     ReverseAcc (x ': xs) acc = ReverseAcc xs (x ': acc)
 
 -- | Extract column names as a type-level list of Symbols.
-type family ColumnNames (cols :: [Type]) :: [Symbol] where
+type family ColumnNames (cols :: [(Symbol, Type)]) :: [Symbol] where
     ColumnNames '[] = '[]
-    ColumnNames (Column n _ ': rest) = n ': ColumnNames rest
+    ColumnNames ('(n, _) ': rest) = n ': ColumnNames rest
 
 -- | Assert that a column name is absent from the schema (for derive/insert).
-type family AssertAbsent (name :: Symbol) (cols :: [Type]) :: Constraint where
+type family AssertAbsent (name :: Symbol) (cols :: [(Symbol, Type)]) :: Constraint where
     AssertAbsent name cols = AssertAbsentHelper name (HasName name cols) cols
 
 type family
-    AssertAbsentHelper (name :: Symbol) (found :: Bool) (cols :: [Type]) ::
+    AssertAbsentHelper (name :: Symbol) (found :: Bool) (cols :: [(Symbol, Type)]) ::
         Constraint
     where
     AssertAbsentHelper name 'False cols = ()
@@ -211,11 +222,11 @@
             )
 
 -- | Assert that a column name is present in the schema.
-type family AssertPresent (name :: Symbol) (cols :: [Type]) :: Constraint where
+type family AssertPresent (name :: Symbol) (cols :: [(Symbol, Type)]) :: Constraint where
     AssertPresent name cols = AssertPresentHelper name (HasName name cols) cols
 
 type family
-    AssertPresentHelper (name :: Symbol) (found :: Bool) (cols :: [Type]) ::
+    AssertPresentHelper (name :: Symbol) (found :: Bool) (cols :: [(Symbol, Type)]) ::
         Constraint
     where
     AssertPresentHelper name 'True cols = ()
@@ -224,7 +235,7 @@
             ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")
 
 -- | Assert that a column name is present in the schema.
-type family AssertAllPresent (name :: [Symbol]) (cols :: [Type]) :: Constraint where
+type family AssertAllPresent (name :: [Symbol]) (cols :: [(Symbol, Type)]) :: Constraint where
     AssertAllPresent (name ': rest) cols =
         AssertAllPresentHelper (HasName name cols) name rest cols
     AssertAllPresent '[] cols = ()
@@ -234,7 +245,7 @@
         (found :: Bool)
         (name :: Symbol)
         (rest :: [Symbol])
-        (cols :: [Type]) ::
+        (cols :: [(Symbol, Type)]) ::
         Constraint
     where
     AssertAllPresentHelper 'True name rest cols = AssertAllPresent rest cols
@@ -249,7 +260,10 @@
 error fires exactly once.
 -}
 type family
-    AssertKeyTypesMatch (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) ::
+    AssertKeyTypesMatch
+        (keys :: [Symbol])
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
         Constraint
     where
     AssertKeyTypesMatch '[] left right = ()
@@ -276,12 +290,18 @@
                 ':<>: 'Text " in the right table"
             )
 
-type family AssertDisjoint (left :: [Type]) (right :: [Type]) :: Constraint where
+type family
+    AssertDisjoint (left :: [(Symbol, Type)]) (right :: [(Symbol, Type)]) ::
+        Constraint
+    where
     AssertDisjoint left right =
         AssertDisjointHelper (SharedNames left right) left right
 
 type family
-    AssertDisjointHelper (shared :: [Symbol]) (left :: [Type]) (right :: [Type]) ::
+    AssertDisjointHelper
+        (shared :: [Symbol])
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
         Constraint
     where
     AssertDisjointHelper '[] left right = ()
@@ -293,7 +313,10 @@
             )
 
 type family
-    AssertAllColumnsHaveType (names :: [Symbol]) (a :: Type) (cols :: [Type]) ::
+    AssertAllColumnsHaveType
+        (names :: [Symbol])
+        (a :: Type)
+        (cols :: [(Symbol, Type)]) ::
         Constraint
     where
     AssertAllColumnsHaveType '[] a cols = ()
@@ -346,16 +369,16 @@
 total — a non-numeric or nullable column is a compile error (with the offending
 column named, via 'AssertRealColumn'), not a runtime 'Left'.
 -}
-type family AllColumnsReal (fn :: Symbol) (cols :: [Type]) :: Constraint where
+type family AllColumnsReal (fn :: Symbol) (cols :: [(Symbol, Type)]) :: Constraint where
     AllColumnsReal fn '[] = ()
-    AllColumnsReal fn (Column n a ': rest) =
+    AllColumnsReal fn ('(n, a) ': rest) =
         (AssertRealColumn fn n a, Real a, VU.Unbox a, AllColumnsReal fn rest)
 
 -- TODO: mchavinda - we can generalist to AllX
-type family AllDouble (cols :: [Type]) :: Constraint where
+type family AllDouble (cols :: [(Symbol, Type)]) :: Constraint where
     AllDouble '[] = ()
-    AllDouble (Column n Double ': rest) = AllDouble rest
-    AllDouble (Column n a ': rest) =
+    AllDouble ('(n, Double) ': rest) = AllDouble rest
+    AllDouble ('(n, a) ': rest) =
         TypeError
             ( 'Text "Column '"
                 ':<>: 'Text n
@@ -366,48 +389,51 @@
 
 {- | Strip 'Maybe' from all columns. Used by 'filterAllJust'.
 
-@Column "x" (Maybe Double)@ becomes @Column "x" Double@.
-@Column "y" Int@ stays @Column "y" Int@.
+@'("x", (Maybe Double)@ becomes @'("x", Double@.))
+@'("y", Int@ stays @'("y", Int@.))
 -}
-type family StripAllMaybe (cols :: [Type]) :: [Type] where
+type family StripAllMaybe (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
     StripAllMaybe '[] = '[]
-    StripAllMaybe (Column n (Maybe a) ': rest) = Column n a ': StripAllMaybe rest
-    StripAllMaybe (Column n a ': rest) = Column n a ': StripAllMaybe rest
+    StripAllMaybe ('(n, Maybe a) ': rest) = '(n, a) ': StripAllMaybe rest
+    StripAllMaybe ('(n, a) ': rest) = '(n, a) ': StripAllMaybe rest
 
 {- | Strip 'Maybe' from a single named column. Used by 'filterJust'.
 
-@StripMaybeAt "x" '[Column "x" (Maybe Double), Column "y" Int]@
-  = @'[Column "x" Double, Column "y" Int]@
+@StripMaybeAt "x" '[ '("x", Maybe Double), '("y", Int)]@
+  = @'[ '("x", Double), '("y", Int)]@
 -}
-type family StripMaybeAt (name :: Symbol) (cols :: [Type]) :: [Type] where
-    StripMaybeAt name (Column name (Maybe a) ': rest) = Column name a ': rest
-    StripMaybeAt name (Column name a ': rest) = Column name a ': rest
+type family StripMaybeAt (name :: Symbol) (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
+    StripMaybeAt name ('(name, Maybe a) ': rest) = '(name, a) ': rest
+    StripMaybeAt name ('(name, a) ': rest) = '(name, a) ': rest
     StripMaybeAt name (col ': rest) = col ': StripMaybeAt name rest
     StripMaybeAt name '[] =
         TypeError
             ('Text "Column '" ':<>: 'Text name ':<>: 'Text "' not found in schema")
 
 -- | Extract column names that appear in both schemas.
-type family SharedNames (left :: [Type]) (right :: [Type]) :: [Symbol] where
+type family SharedNames (left :: [(Symbol, Type)]) (right :: [(Symbol, Type)]) :: [Symbol] where
     SharedNames '[] right = '[]
-    SharedNames (Column n _ ': rest) right =
+    SharedNames ('(n, _) ': rest) right =
         SharedNamesHelper (HasName n right) n rest right
 
 type family
     SharedNamesHelper
         (found :: Bool)
         (n :: Symbol)
-        (rest :: [Type])
-        (right :: [Type]) ::
+        (rest :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
         [Symbol]
     where
     SharedNamesHelper 'True n rest right = n ': SharedNames rest right
     SharedNamesHelper 'False n rest right = SharedNames rest right
 
 -- | Columns from @left@ whose names do NOT appear in @right@.
-type family UniqueLeft (left :: [Type]) (rightNames :: [Symbol]) :: [Type] where
+type family
+    UniqueLeft (left :: [(Symbol, Type)]) (rightNames :: [Symbol]) ::
+        [(Symbol, Type)]
+    where
     UniqueLeft '[] _ = '[]
-    UniqueLeft (Column n a ': rest) rn =
+    UniqueLeft ('(n, a) ': rest) rn =
         UniqueLeftHelper (IsElem n rn) n a rest rn
 
 type family
@@ -415,26 +441,29 @@
         (found :: Bool)
         (n :: Symbol)
         (a :: Type)
-        (rest :: [Type])
+        (rest :: [(Symbol, Type)])
         (rn :: [Symbol]) ::
-        [Type]
+        [(Symbol, Type)]
     where
     UniqueLeftHelper 'True n a rest rn = UniqueLeft rest rn
-    UniqueLeftHelper 'False n a rest rn = Column n a ': UniqueLeft rest rn
+    UniqueLeftHelper 'False n a rest rn = '(n, a) ': UniqueLeft rest rn
 
 type family ToMaybe (a :: Type) :: Type where
     ToMaybe (Maybe a) = Maybe a
     ToMaybe a = Maybe a
 
 -- | Wrap column types in Maybe; idempotent on already-optional columns.
-type family WrapMaybe (cols :: [Type]) :: [Type] where
+type family WrapMaybe (cols :: [(Symbol, Type)]) :: [(Symbol, Type)] where
     WrapMaybe '[] = '[]
-    WrapMaybe (Column n a ': rest) = Column n (ToMaybe a) ': WrapMaybe rest
+    WrapMaybe ('(n, a) ': rest) = '(n, ToMaybe a) ': WrapMaybe rest
 
 -- | Wrap selected columns in Maybe by name list.
-type family WrapMaybeColumns (names :: [Symbol]) (cols :: [Type]) :: [Type] where
+type family
+    WrapMaybeColumns (names :: [Symbol]) (cols :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     WrapMaybeColumns names '[] = '[]
-    WrapMaybeColumns names (Column n a ': rest) =
+    WrapMaybeColumns names ('(n, a) ': rest) =
         WrapMaybeColumnsHelper (IsElem n names) n a names rest
 
 type family
@@ -443,18 +472,24 @@
         (n :: Symbol)
         (a :: Type)
         (names :: [Symbol])
-        (rest :: [Type]) ::
-        [Type]
+        (rest :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
     where
     WrapMaybeColumnsHelper 'True n a names rest =
-        Column n (ToMaybe a) ': WrapMaybeColumns names rest
+        '(n, ToMaybe a) ': WrapMaybeColumns names rest
     WrapMaybeColumnsHelper 'False n a names rest =
-        Column n a ': WrapMaybeColumns names rest
+        '(n, a) ': WrapMaybeColumns names rest
 
 -- | Columns in left whose names collide with right (excluding keys).
-type family CollidingColumns (left :: [Type]) (right :: [Type]) (keys :: [Symbol]) :: [Type] where
+type family
+    CollidingColumns
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)])
+        (keys :: [Symbol]) ::
+        [(Symbol, Type)]
+    where
     CollidingColumns '[] _ _ = '[]
-    CollidingColumns (Column n a ': rest) right keys =
+    CollidingColumns ('(n, a) ': rest) right keys =
         CollidingColumnsHelper1 (IsElem n keys) n a rest right keys
 
 type family
@@ -462,10 +497,10 @@
         (isKey :: Bool)
         (n :: Symbol)
         (a :: Type)
-        (rest :: [Type])
-        (right :: [Type])
+        (rest :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)])
         (keys :: [Symbol]) ::
-        [Type]
+        [(Symbol, Type)]
     where
     CollidingColumnsHelper1 'True n a rest right keys =
         CollidingColumns rest right keys
@@ -477,18 +512,24 @@
         (inRight :: Bool)
         (n :: Symbol)
         (a :: Type)
-        (rest :: [Type])
-        (right :: [Type])
+        (rest :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)])
         (keys :: [Symbol]) ::
-        [Type]
+        [(Symbol, Type)]
     where
     CollidingColumnsHelper2 'True n a rest right keys =
-        Column n (These a (Lookup n right)) ': CollidingColumns rest right keys
+        '(n, These a (Lookup n right)) ': CollidingColumns rest right keys
     CollidingColumnsHelper2 'False n a rest right keys =
         CollidingColumns rest right keys
 
 -- | Inner join result schema.
-type family InnerJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) :: [Type] where
+type family
+    InnerJoinSchema
+        (keys :: [Symbol])
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     InnerJoinSchema keys left right =
         Append
             (SubsetSchema keys left)
@@ -501,7 +542,13 @@
             )
 
 -- | Left join result schema.
-type family LeftJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) :: [Type] where
+type family
+    LeftJoinSchema
+        (keys :: [Symbol])
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     LeftJoinSchema keys left right =
         Append
             (SubsetSchema keys left)
@@ -514,7 +561,13 @@
             )
 
 -- | Right join result schema.
-type family RightJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) :: [Type] where
+type family
+    RightJoinSchema
+        (keys :: [Symbol])
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     RightJoinSchema keys left right =
         Append
             (SubsetSchema keys right)
@@ -528,8 +581,11 @@
 
 -- | Full outer join result schema.
 type family
-    FullOuterJoinSchema (keys :: [Symbol]) (left :: [Type]) (right :: [Type]) ::
-        [Type]
+    FullOuterJoinSchema
+        (keys :: [Symbol])
+        (left :: [(Symbol, Type)])
+        (right :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
     where
     FullOuterJoinSchema keys left right =
         Append
@@ -543,9 +599,12 @@
             )
 
 -- | Extract Column entries from a schema whose names appear in @keys@.
-type family GroupKeyColumns (keys :: [Symbol]) (cols :: [Type]) :: [Type] where
+type family
+    GroupKeyColumns (keys :: [Symbol]) (cols :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
+    where
     GroupKeyColumns keys '[] = '[]
-    GroupKeyColumns keys (Column n a ': rest) =
+    GroupKeyColumns keys ('(n, a) ': rest) =
         GroupKeyColumnsHelper (IsElem n keys) n a keys rest
 
 type family
@@ -554,15 +613,15 @@
         (n :: Symbol)
         (a :: Type)
         (keys :: [Symbol])
-        (rest :: [Type]) ::
-        [Type]
+        (rest :: [(Symbol, Type)]) ::
+        [(Symbol, Type)]
     where
     GroupKeyColumnsHelper 'True n a keys rest =
-        Column n a ': GroupKeyColumns keys rest
+        '(n, a) ': GroupKeyColumns keys rest
     GroupKeyColumnsHelper 'False n a keys rest = GroupKeyColumns keys rest
 
 -- | Provides runtime evidence of a schema: a list of (name, TypeRep) pairs.
-class KnownSchema (cols :: [Type]) where
+class KnownSchema (cols :: [(Symbol, Type)]) where
     schemaEvidence :: [(T.Text, SomeTypeRep)]
 
 instance KnownSchema '[] where
@@ -570,7 +629,7 @@
 
 instance
     (KnownSymbol name, Typeable a, Columnable a, KnownSchema rest) =>
-    KnownSchema (Column name a ': rest)
+    KnownSchema ('(name, a) ': rest)
     where
     schemaEvidence =
         (T.pack (symbolVal (Proxy @name)), someTypeRep (Proxy @a))
diff --git a/src/DataFrame/Typed/Types.hs b/src/DataFrame/Typed/Types.hs
--- a/src/DataFrame/Typed/Types.hs
+++ b/src/DataFrame/Typed/Types.hs
@@ -14,9 +14,6 @@
     -- * Core phantom-typed wrapper
     TypedDataFrame (..),
 
-    -- * Column phantom type (no constructors)
-    Column,
-
     -- * Typed expressions (schema-validated)
     TExpr (..),
 
@@ -49,11 +46,11 @@
 
 {- | A phantom-typed wrapper over the untyped 'DataFrame'.
 
-The type parameter @cols@ is a type-level list of @Column name ty@ entries
+The type parameter @cols@ is a type-level list of @'(name, ty)@ pairs
 that tracks the schema at compile time. All operations delegate to the
 untyped core at runtime and update the phantom type at compile time.
 -}
-newtype TypedDataFrame (cols :: [Type]) = TDF {unTDF :: D.DataFrame}
+newtype TypedDataFrame (cols :: [(Symbol, Type)]) = TDF {unTDF :: D.DataFrame}
 
 instance Show (TypedDataFrame cols) where
     show (TDF df) = show df
@@ -61,12 +58,6 @@
 instance Eq (TypedDataFrame cols) where
     (TDF a) == (TDF b) = a == b
 
-{- | A phantom type that pairs a type-level column name ('Symbol')
-with its element type. Has no value-level constructors — used
-purely at the type level to describe schemas.
--}
-data Column (name :: Symbol) (a :: Type)
-
 {- | A typed expression validated against schema @cols@, producing values of type @a@.
 
 Unlike the untyped 'Expr a', a 'TExpr' can only be constructed through
@@ -75,7 +66,7 @@
 
 Use 'unTExpr' to extract the underlying 'Expr' for delegation to the untyped API.
 -}
-newtype TExpr (cols :: [Type]) a = TExpr {unTExpr :: Expr a}
+newtype TExpr (cols :: [(Symbol, Type)]) a = TExpr {unTExpr :: Expr a}
 
 -- | Shows the underlying expression; the schema phantom is type-level only.
 instance (Show a) => Show (TExpr cols a) where
@@ -85,23 +76,23 @@
 @AsTExpr cols (Expr r) = TExpr cols r@. Lets a result type follow the frame —
 an @Expr@ over a plain frame becomes a @TExpr@ over a typed one.
 -}
-type family AsTExpr (cols :: [Type]) (e :: Type) :: Type where
+type family AsTExpr (cols :: [(Symbol, Type)]) (e :: Type) :: Type where
     AsTExpr cols (Expr r) = TExpr cols r
 
 -- | Lift an untyped expression into its 'TExpr' for schema @cols@.
-class ToTExpr (cols :: [Type]) e where
+class ToTExpr (cols :: [(Symbol, Type)]) e where
     toTExpr :: e -> AsTExpr cols e
 
 instance ToTExpr cols (Expr r) where
     toTExpr = TExpr
 
 -- | A typed sort order validated against schema @cols@.
-data TSortOrder (cols :: [Type]) where
+data TSortOrder (cols :: [(Symbol, Type)]) where
     Asc :: (Columnable a, Ord a) => TExpr cols a -> TSortOrder cols
     Desc :: (Columnable a, Ord a) => TExpr cols a -> TSortOrder cols
 
 -- | A phantom-typed wrapper over 'GroupedDataFrame'.
-newtype TypedGrouped (keys :: [Symbol]) (cols :: [Type])
+newtype TypedGrouped (keys :: [Symbol]) (cols :: [(Symbol, Type)])
     = TGD {unTGD :: D.GroupedDataFrame}
 
 {- | Internal aggregation chain. Each cons prepends a 'Column' to the
@@ -115,7 +106,7 @@
   . as \@\"avg_age\" (F.mean age)
 @
 -}
-data TAgg (keys :: [Symbol]) (cols :: [Type]) (aggs :: [Type]) where
+data TAgg (keys :: [Symbol]) (cols :: [(Symbol, Type)]) (aggs :: [(Symbol, Type)]) where
     TAggNil :: TAgg keys cols '[]
     TAggCons ::
         (Columnable a) =>
@@ -125,7 +116,7 @@
         TExpr cols a ->
         -- | rest
         TAgg keys cols aggs ->
-        TAgg keys cols (Column name a ': aggs)
+        TAgg keys cols ('(name, a) ': aggs)
 
 {- | Extract the runtime 'NamedExpr' list from a 'TAgg', in
 declaration order (reversed from the cons-built order).
