A small Fable utility library providing F# representations of TypeScript type-system idioms that have no direct equivalent in standard F#. All types are erased at runtime — they exist only for the compiler and carry zero overhead in the emitted JavaScript.
keyof<'T>— TypeScriptkeyof Ttypekeyof<'T, 'ReturnType>— TypeScriptkeyof Twith value-type retentionproptypekey<'T, 'ReturnType>andproptypelock<'T>— TypeScriptT[keyof T](union of all property value types)PropertyRecord<'T, 'K>— index-signature objects- Active patterns:
KeyIs/TypeKeyIs - Helper modules:
KeyOf/TypeKeyOf
type Keys = keyof MyObj // "field1" | "field2" | ...
function get<T>(obj: T, key: keyof T): unknown { ... }keyof T produces a string-literal union of the property names of T. It is used to constrain a value to only valid property names of a given type.
keyof<'T> is an erased wrapper over string that the compiler constrains to properties of 'T. At runtime it is just a plain string.
// Construction — use the keyof helper function
let key = keyof<MyObj> _.FieldName // keyof<MyObj>, value = "FieldName"
// Usage as a field type
type Accessor<'T> = { Key: keyof<'T> }
// Accessing the underlying string
key.Value // "FieldName"
// Reading a property from an object
KeyOf.item key myObj // obj optiontype Config = {
Timeout: int
Retries: int
Verbose: bool
}
let describe (key: keyof<Config>) =
printfn "Config key: %s" key.Value
describe (keyof<_> _.Timeout) // "Config key: Timeout"
describe (keyof<_> _.Verbose) // "Config key: Verbose"// Indexed access type — retains the value type at key K
type Val = T[K]
function getTyped<T, K extends keyof T>(obj: T, key: K): T[K] { ... }keyof<'T> loses the value type at that key (returning obj option). typekeyof<'T, 'ReturnType> is keyof with the value type preserved — equivalent to TypeScript's K extends keyof T + T[K] together.
At runtime it is still a plain string; the 'ReturnType parameter is purely a compile-time witness.
// Construction
let key = typekeyof<Config, int> _.Timeout // typekeyof<Config, int>
// Accessing with the correct return type (no casting needed)
TypeKeyOf.item key config // int
// Erasing the return type to get a plain keyof
let plainKey: keyof<Config> = TypeKeyOf.box keytype Vec2 = { X: float; Y: float }
let scale (factor: float) (key: typekeyof<Vec2, float>) (v: Vec2) : Vec2 =
let current = TypeKeyOf.item key v
// emitted JS: v[key] * factor — no runtime overhead
match key with
| TypeKeyIs "X" -> { v with X = current * factor }
| TypeKeyIs "Y" -> { v with Y = current * factor }
| _ -> v
let doubleX = scale 2.0 (typekeyof<Vec2, float> _.X)typekeyof is designed to work with SRTP constraints, enabling generic property accessors that work across multiple types sharing the same property name:
// A generic accessor for any type with an 'ofValue' property
type OfValueAccessor =
static member inline create<
^T, ^ReturnType when ^T:(member ofValue: ^ReturnType)
>(?_: ^T): typekeyof<^T, ^ReturnType>
= unbox "ofValue"
type Box = { ofValue: int }
type Crate = { ofValue: string }
// Both resolved correctly by the compiler:
OfValueAccessor.create<Box, int>() // typekeyof<Box, int>
OfValueAccessor.create<Crate, string>() // typekeyof<Crate, string>// T[keyof T] — a value that is *some* property of T, type unknown until unlocked
type AnyPropOf<T> = T[keyof T]
// A heterogeneous property bag where you carry the witness separately:
declare function readProp<T, K extends keyof T>(obj: T, key: K): T[K]When a value could be any of the property value types of T — a union T["a"] | T["b"] | T["c"] — but the specific key isn't known until runtime, TypeScript tracks this through generic constraints. In F# the closest equivalent is obj, losing all type information.
proptypelock<'T> + proptypekey<'T, 'ReturnType> model this pattern. A proptypelock<'T> is an opaque value of some property of T. A proptypekey<'T, 'ReturnType> is a witness that, given a proptypelock<'T>, returns the concrete 'ReturnType. The lock and key together perform the same role as TypeScript's K extends keyof T / T[K] pairing.
// Single property — exact type
let nameKey = proptypekey (fun (p: Person) -> p.Name) // proptypekey<Person, string>
// Multiple properties — union type (up to 8, producing U2..U8)
let nameOrAge = proptypekey (fun (p: Person) -> p.Name) (fun p -> p.Age)
// proptypekey<Person, U2<string, int>>// Lock: wrap a concrete value into proptypelock
let locked: proptypelock<Person> = nameOrAge.lock "Alice"
// Unlock: recover the typed value using the witness key
let value: U2<string, int> = nameOrAge.unlock locked
// or via indexer on the lock:
let value2: U2<string, int> = locked[nameOrAge]// TypeScript original
interface Config {
host: string
port: number
debug: boolean
}
function readConfigProp<K extends keyof Config>(cfg: Config, key: K): Config[K] { ... }type Config = { Host: string; Port: int; Debug: bool }
// Define the multi-type key witness
let configPropKey =
proptypekey
(fun (c: Config) -> c.Host)
(fun c -> c.Port)
(fun c -> c.Debug)
// : proptypekey<Config, U3<string, int, bool>>
// Lock a value we read at runtime
let locked = configPropKey.lock cfg.Host // proptypelock<Config>
// Unlock with full type information
let v: U3<string, int, bool> = configPropKey.unlock locked
match v with
| U3.Case1 s -> printfn "host: %s" s
| U3.Case2 n -> printfn "port: %d" n
| U3.Case3 b -> printfn "debug: %b" b// Index signature — an object accessed by property keys of T
interface PropertyRecord<T, K> {
[key: keyof T]: K
}A dictionary-like object where the valid keys are exactly the property names of T and all values share the same type K.
PropertyRecord<'T, 'K> is an interface with a [<EmitIndexer>] getter/setter. It emits plain JS bracket access (obj[key]) while keeping the key constrained to keyof<'T>.
// Declare a binding for a JS object that stores values by Config keys
[<ImportMember("./config-store.js")>]
let store: PropertyRecord<Config, string> = jsNative
// Type-safe read and write
let host = store[keyof<Config> _.Host] // string
store[keyof<Config> _.Port] <- "8080"Pattern match on a keyof or typekeyof value against a string literal.
let describeKey (key: keyof<Config>) =
match key with
| KeyIs "Host" -> "the host name"
| KeyIs "Port" -> "the port number"
| KeyIs "Debug" -> "the debug flag"
| _ -> "unknown key"
let describeTypedKey (key: typekeyof<Config, int>) =
match key with
| TypeKeyIs "Port" -> "the port number"
| _ -> "some int field"Functional-style helpers for working with key values without using extension methods.
| Function | Signature | Description |
|---|---|---|
KeyOf.value |
keyof<'T> -> string |
Extract the underlying string |
KeyOf.item |
keyof<'T> -> 'T -> obj option |
Read a property (untyped) |
KeyOf.access |
alias for item |
|
KeyOf.fromPropertyKey |
typekeyof<'T,'R> -> keyof<'T> |
Erase the return type |
KeyOf.unsafeUnbox |
keyof<'T> -> typekeyof<'T,'R> |
Add a return type (unchecked) |
KeyOf.tryUnbox |
('T->'R) -> keyof<'T> -> typekeyof<'T,'R> option |
Checked version using nameofLambda |
| Function | Signature | Description |
|---|---|---|
TypeKeyOf.create |
('T -> 'R) -> typekeyof<'T,'R> |
Construct from a lambda |
TypeKeyOf.value |
typekeyof<'T,'R> -> string |
Extract the underlying string |
TypeKeyOf.box |
typekeyof<'T,'R> -> keyof<'T> |
Erase the return type |
TypeKeyOf.item |
typekeyof<'T,'R> -> 'T -> 'R |
Read a property (typed) |
TypeKeyOf.access |
alias for item |
| TypeScript idiom | Library type |
|---|---|
keyof T (name only) |
keyof<'T> |
K extends keyof T + T[K] (name + value type) |
typekeyof<'T, 'ReturnType> |
T[keyof T] (heterogeneous value union) |
proptypelock<'T> + proptypekey<'T, 'ReturnType> |
{ [key: keyof T]: V } (index-signature object) |
PropertyRecord<'T, 'V> |