Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - extending object's type progressively

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!

like image 709
Dragan Okanovic Avatar asked Dec 06 '19 16:12

Dragan Okanovic


People also ask

Can you extend types in TypeScript?

Extending types in TypeScript can be done by using the & operator. This is called intersection types that can be used for combining two or more different types together. The Tomato type will now contain both fields from the other two types. You can combine as many types together this way as needed.

How do you define object of objects type in TypeScript?

In TypeScript, object is the type of all non-primitive values (primitive values are undefined , null , booleans, numbers, bigints, strings). With this type, we can't access any properties of a value.

What is type assertion in TypeScript?

In Typescript, Type assertion is a technique that informs the compiler about the type of a variable. Type assertion is similar to typecasting but it doesn't reconstruct code. You can use type assertion to specify a value's type and tell the compiler not to deduce it.

How do you use extended interface TypeScript?

Extending Interfaces in TypeScript # Use the extends keyword to extend interfaces in TypeScript, e.g. interface Dog extends Animal {age: number;} . The extends keyword allows us to copy the members from other named types and add new members to the final, more generic interface.


1 Answers

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!

Link to code

like image 139
jcalz Avatar answered Nov 15 '22 04:11

jcalz