Values
Every column accessor returns a Table.Value.Value. It is one union type with
six constructors, and it is the type the built-in sort, filter, and aggregation
functions compare. This page is about that type: why it exists, what each
constructor is for, and the two coercion helpers the built-ins rely on.
TanStack has no page for this. In TypeScript a cell value is whatever the accessor returns, and the built-in functions reach for JavaScript's own coercions when they need to compare two of them.
Why there is one union type at all
A built-in sort function has to order two cells without knowing anything about
your row type. A built-in filter has to decide whether a cell matches a filter
value. An aggregation has to fold a list of cells into one. In TypeScript those
functions take unknown and lean on <, ==, String(x) and Number(x).
Elm has no unknown and no coercions, so the port needs a concrete type the
comparisons can be written against. That type is Value.
type Value
= String String
| Number Float
| Bool Bool
| Date Time.Posix
| List (List Value)
| Null
Value is fully exposed, so you construct and pattern match on it directly.
Writing an accessor
Table.column takes a column id and an
accessor row -> Value. Most accessors are a field access composed with a
constructor:
personColumns : List (Table.Column Person)
personColumns =
[ Table.column "firstName" (.firstName >> Value.String)
, Table.column "age" (.age >> toFloat >> Value.Number)
, Table.column "salary" (.salary >> Value.Number)
, Table.column "active" (.active >> Value.Bool)
, Table.column "tags" (.tags >> List.map Value.String >> Value.List)
]
An Int field needs toFloat first, because Number holds a Float.
Date holds a Time.Posix, so a date column reads a Time.Posix field and
sorts with SortFn.datetime:
eventColumns : List (Table.Column Event)
eventColumns =
[ Table.column "name" (.name >> Value.String)
, Table.column "startsAt" (.startsAt >> Value.Date)
|> Table.withSortFn SortFn.datetime
]
A field that may be missing becomes Null:
maybeString : (row -> Maybe String) -> row -> Value.Value
maybeString get row =
case get row of
Just text ->
Value.String text
Nothing ->
Value.Null
Null covers null and undefined
Value has no separate case for "missing" and "explicitly null". JavaScript's
null and undefined both read back as Null everywhere a cell value is read.
That is deliberate and it shows up in a few places:
Table.SortFnplacesNullcells withTable.withSortUndefined; there is nothing else for it to place.- The
uniqueaggregation over[ "a", null, undefined, "a" ]gives[ "a", Null ]in Elm and three entries in TanStack, souniqueCountis2rather than3. - Grouping puts
Nullvalues under the key"null", which merges TanStack's separatenullandundefinedbuckets. See Grouping.
Table.Value.isNull is the test:
Value.isNull (Table.getValue config row "department")
Value.List is the array cell value
List (List Value) is the counterpart of a JavaScript array cell value. It
exists because several built-ins need one to work on:
FilterFn.arrIncludes,arrIncludesAll,arrIncludesSome, andarrHastest a cell against a filter value that is itself a list.AggregationFn.extentreturns a two-elementList(the minimum and the maximum), anduniquereturns the distinct values as aList.- Range filter values, the
[ min, max ]pair thatinNumberRangeandbetweenexpect, are aListof two values.
tagsColumn : Table.Column Person
tagsColumn =
Table.column "tags" (.tags >> List.map Value.String >> Value.List)
|> Table.withHeader "Tags"
|> Table.withFilterFn FilterFn.arrIncludes
toString and toNumber
The built-in functions compare cells the way JavaScript would, so the port needs
JavaScript's two coercions.
Value.toString is String(x) and
Value.toNumber is Number(x).
toString
| Value | toString |
|---|---|
String "ada" |
"ada" |
Number 1 |
"1" |
Number 1.5 |
"1.5" |
Bool True |
"true" |
Bool False |
"false" |
Date posix |
the millisecond timestamp as a string |
List items |
each item's toString, joined with "," |
Null |
"" |
Null is the one place this is not literally String(x): JavaScript's
String(null) is "null", and TanStack writes String(x ?? '') at every call
site that matters, so "" is the behaviour the built-ins actually see.
toNumber
| Value | toNumber |
|---|---|
Number 29 |
29 |
String "29" |
29 |
String "" |
0 |
String "abc" |
NaN |
Bool True |
1 |
Bool False |
0 |
Date posix |
the millisecond timestamp |
List [] |
0 |
List [ item ] |
toNumber item |
List _ (two or more) |
NaN |
Null |
0 |
A string is parsed with Elm's String.toFloat after trimming, which covers
every decimal form JavaScript accepts but not the JavaScript-only literals:
"0x10" and "Infinity" give NaN here where Number(x) gives 16 and
Infinity. NaN compares False against everything, exactly as it does in
JavaScript.
Dates
Two more differences follow from Date holding a Time.Posix:
Time.Posixcannot hold JavaScript'snew Date(NaN). An invalid date is aNumber NaNinstead, which sorts and filters the way an invalid date does.FilterFn.inDateRangeparses string endpoints withFilterFn.toDateTimestamp, which accepts ISO 8601 only:YYYY-MM-DD, optionally withTHH:MMorTHH:MM:SSand a trailingZ. Anything else isNaN, so it is never in range. There is nonew Date(string)in Elm to fall back on.
Formatting a cell
Value.toString is there for the built-ins, and it is fine for a quick render.
For anything a person reads, pattern match instead. That is how you get
thousands separators, a real date format, and a placeholder for a missing value:
formatCell : Value.Value -> String
formatCell value =
case value of
Value.String text ->
text
Value.Number n ->
String.fromFloat n
Value.Bool True ->
"yes"
Value.Bool False ->
"no"
Value.Date posix ->
String.fromInt (Time.posixToMillis posix)
Value.List items ->
String.join ", " (List.map formatCell items)
Value.Null ->
"-"
Your own logic never has to go through Value
Value is the type the built-in functions speak. Your own sort and filter
functions receive the Table.Row row, so they can read the original record and
any other column on it:
fullNameColumn : Table.Column Person
fullNameColumn =
Table.column "lastName" (.lastName >> Value.String)
|> Table.withHeader "Last name"
|> Table.withCustomSort
(\a b ->
compare (fullName (Table.rowOriginal a)) (fullName (Table.rowOriginal b))
)
|> Table.withCustomFilter
(\row filterValue ->
String.contains
(String.toLower (Value.toString filterValue))
(String.toLower (fullName (Table.rowOriginal row)))
)
Table.withCustomSort takes
Row row -> Row row -> Order and
Table.withCustomFilter takes
Row row -> Value -> Bool, where the Value is the filter value from state.
Table.rowOriginal gets you back to your
own record. A custom function wins over a built-in one on the same column.
The built-in SortFn and
FilterFn values work on one column's
Value, and both modules expose a custom constructor if you want to write a
comparison at that level instead.
Not covered here
TanStack's "Table and Column Meta" and "Type Helpers" pages have no counterpart, because Elm's own types do that job; that is noted on Features.