I'd like to add a new field to heterogeneous object, knowing at compile time that the key is not present (or at least not present in the type of the object). The purpose of this is to keep track of initialised resources and their types st you can't access an uninitialised resource in a typesafe way, and you know all initialised resources statically.
Ideally, the function would look like this:
type FreshKey<A,K> = K extends keyof A ? never : K
const f <A, K extends string, v> = (x: A)
=> (k: FreshKey<A,K>, v: V)
=> A & { [k: K]: V} = ...
With usage being something like this:
const z = {}
const y = f(z)("foo", []);
const x = f(y)("bar", 3);
const v = f(x)("baz", "");
const v2 = f(x)("foo", []) // shouldn't typecheck because "foo" is already present
The problem is that I can't even describe the resulting type of f in a way that typescript is happy with. I hit typescript[1337] (An index signature parameter type cannot be a literal type of generic type...).
The ideal implementation would typecheck too, but I'd be happy to just cast it
Create a generic type to ensure our provided key is unqiue
type EnsureFreshKey<OBJECT extends object, KEY extends string | number | symbol> = KEY extends keyof OBJECT ? never : KEY;
Create a function to handle adding the new key
function AddNewKey<OBJECT extends object>(a: OBJECT) {
return <NEW_KEY extends string | number | symbol, VALUE>(key: EnsureFreshKey<OBJECT, NEW_KEY>, value: VALUE) => {
// Check for KEY uniqueness at runtime
if (key in a && a[key as unknown as keyof OBJECT] !== undefined) {
throw new Error(`Key "${String(key)}" already exists in object ${JSON.stringify(a)}`)
}
// You can decide whether to create a new object or just assign
// the key onto the existing object
return {
...a,
[key]: value
} as OBJECT & { [key in NEW_KEY]: VALUE }
}
}
Note:
This piece of code will result in never
if NEW_KEY
already exists in OBJECT
.
And your variable, NEW_KEY
should never be of type never
key: EnsureFreshKey<A, FK>
Test Cases
const a = {}
const b = AddNewKey(z)("foo", []);
const c = AddNewKey(y)("bar", 3);
const d = AddNewKey(x)("baz", "");
const e = AddNewKey(x)("foo", []); // correctly throws an error