Reputation: 7781
I am trying to achieve the following with TS:
let m: Extendable
m.add('one', 1)
// m now has '.one' field
m.add('two', 2)
// 'm' now has '.one' and '.two' fields
I'm familiar with returning extended types in TS via:
function extend<T, V>(obj: T, val: V): T & {extra: V} {
return {
...obj,
extra: val
}
}
Now, there are two issues in my case:
1) object m
needs to update its type after the add()
has been called to reflect addition of a new field
2) new field's name is parameterized (ain't always extra
e.g.)
First issue might be resolved by using class definition and somehow using TypeThis
utility to re-adjust the type, but I wasn't able to find enough documentation about how to use it.
Any help or guidance is welcome. Thanks!
Upvotes: 6
Views: 1726
Reputation: 329418
TypeScript 3.7 introduced assertion functions which can be used to narrow the type of passed-in arguments or even this
. Assertion functions look kind of like user-defined type guards, but you add an asserts
modifier before the type predicate. Here's how you could implement Extendable
as a class with add()
as an assertion method:
class Extendable {
add<K extends PropertyKey, V>(key: K, val: V): asserts this is Record<K, V> {
(this as unknown as Record<K, V>)[key] = val;
}
}
When you call m.add(key, val)
the compiler asserts that m
will have a property with key with the type of key
and a corresponding value with the type of val
. Here's how you'd use it:
const m: Extendable = new Extendable();
// ~~~~~~~~~~~~ <-- important annotation here!
m.add('one', 1)
m.add('two', 2)
console.log(m.one.toFixed(2)); // 1.00
console.log(m.two.toExponential(2)); // 2.00e+0
That all works as you expect. After you call m.add('one', 1)
, you can refer to m.one
with no compiler warning.
Unfortunately there's a fairly major caveat; assertion functions only work if they have an explicitly annotated type. According to the relevant pull request, "this particular rule exists so that control flow analysis of potential assertion calls doesn't circularly trigger further analysis."
That means the following is an error:
const oops = new Extendable(); // no annotation
oops.add("a", 123); // error!
//~~~~~~~~ <-- Assertions require every name in the call target to be declared with
// an explicit type annotation.
The only difference is that the type of oops
is inferred to be Extendable
instead of annotated as Extendable
as m
is. And you get an error calling oops.add()
. Depending on your use case this could either be no big deal or a showstopper.
Okay, hope that helps; good luck!
Upvotes: 5