4

See this code for example:

var a
a.set('my-custom-value',55)
a.

In line number 3, How I make the IDE to be aware to the value in line 2, and offer auto complete to a string the defined by the end-user who is using the library?

I want that after I type a. I will see "my-custom-value" as an auto-complete option by VSCode.

I saw the yargs is working like that. If I defining a option or a positional parameter, later the IDE offer me what I choosed.

Aminadav Glickshtein
  • 23,232
  • 12
  • 77
  • 117

4 Answers4

4

This can be done with an asserts return type, which is possible because Typescript supports control-flow type narrowing:

class Foo {
    setKey<K extends PropertyKey, V>(k: K, v: V): asserts this is Record<K, V> {
        (this as any)[k] = v;
    }
}

let foo: Foo = new Foo();
// foo. completes as foo.setKey

foo.setKey('bar', 23);
// now foo. completes as foo.bar or foo.setKey

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
2

Its actually possible! Sort of. We can use control-flow type-narrowing (thanks @kaya3) along with a generic type (T) to keep track of what keys have been set. However, due to the type narrowing can only add keys, not remove them.

Here is a working example map with set and get:
(see @kaya3's answer for a simpler example if you don't want a separate get function)

// Tracking map.
class TMap<V, T extends Record<string, V> = {}> {
    public static create<V>(): TMap<V> {
        return new TMap({});
    }

    private _data: T;
    private constructor(data: T) {
        this._data = data;
    }

    public set<K extends string>(key: string extends K ? never : K, val: V): asserts this is TMap<V, Record<K, V> & T> {
        (this._data as Record<K, V>)[key] = val;
    }

    public get<K extends keyof T>(key: K): V {
        return this._data[key];
    }
}

const a: TMap<number> = TMap.create();
a.set('my-custom-value', 55);
// a.get tooltip: get(key: "my-custom-value"): number
a.set('my-other-key', 22);
a.set('my-other-key', 33);

// a.get tooltip: get(key: "my-custom-value" | "my-other-key"): number
const x = a.get('my-custom-value');
const y = a.get('my-other-key');

const unknownString: string = 'mystery-key';
// a.set(unknownString, 101); // Not allowed b/c we don't know the exact string value.

Playground Link

key: string extends K ? never : K is a hack to prevent you from supplying a non-specific string to the map, which would kill the key tracking.


Old clunkier example using copy-on-write:

Playground Link

Mingwei Samuel
  • 2,917
  • 1
  • 30
  • 40
  • 1
    Typescript does control-flow type narrowing, so a variable can have a different, narrower type at another point in the code than where the variable is declared. – kaya3 Sep 07 '20 at 02:10
  • @kaya3 Oh cool! I didn't know about that feature, updating my answer.. – Mingwei Samuel Sep 07 '20 at 20:20
1

To achieve this, you'll need to construct the type of object that you want. The following code allows you to do that.

class Builder<T extends object = {}> {
  private _obj: any
  constructor(t: T) {
    this._obj = { ...t }
    Object.keys(t).forEach((key) => (this as any)[key] = (t as any)[key])
  }

  public set<K extends string, V>(key: K, value: V): Builder<SetResult<T, K, V>> & SetResult<T, K, V> {
    (this._obj)[key] = value
    return new Builder<SetResult<T, K, V>>(this._obj) as Builder<SetResult<T, K, V>> & SetResult<T, K, V>
  }
}
type SetResult<T, K extends string, V> = T & { [k in K]: V }

Use the code this way:

const a = new Builder({})
  .set('abc', 55)
  .set('def', 'name')
  .set('my_complex_variable', { ID: '1234', exists: false })

console.log(a.abc)
console.log(a.def)
console.log(a.my_complex_variable.ID)

The way it works is quite straightforward. Each call to the set function returns a new Builder object, which contains all the previous set calls. Note that each of the fields you access is strongly typed too: abc is a number, def is a string. There is some fairly nasty casting to the any type, but you could potentially clean that up if needed

Here's link to the working code

Edit: I just noticed that if you use a variable name with hyphens in it, Intellisense won't bring it up as a suggestion if you use .. You can still get type checking on the field, but you'll have to access it using the indexer:

const a = new Builder({}).set('my-variable', 12)
console.log(a['my-variable'])
Paul Huynh
  • 2,462
  • 1
  • 11
  • 11
0

if you know what value you are going to be setting you could try something like this

let a: { value?: number } = {}
a.value = 55

but value will always be of type number | undefined because the typescript compiler doesn't know if you've set the value or not at any point in the code.

Ben Baldwin
  • 427
  • 1
  • 6
  • 13