playest
playest

Reputation: 15

Reference access field through index and keep the correct type

I'm converting a existing code from javascript to typescript and I try to not change anything to the logic of the code to avoid regressions. I found myself in a situation where I don't exactly know what to do to make typescript understand that some things are allowed. I tried to reduce the examples to a minimum so I'm sorry if the logic is a little weird.

I have the same kind of problem in 2 places so I group them here. Here is the code and a playgound:

interface Config {
    autoStart: boolean;
    baseUrl: string;
}

let _configTrackers: Partial<{[key in keyof Config]: ((value: Config[key], previous?: Config[key]) => any)[]}> = {};
let settings: Partial<Config> = { autoStart: true };
let _configuration: Partial<Config> = { baseUrl: "/" };

function get<K extends keyof Config>(name: K): Partial<Config>[K] {
    let cb = (value: Config[K], previous?: Config[K]) => { return 1 };
    _configTrackers[name]!.push(cb /* "as any" works here but it's not really what I want */); // error on cb
    // Argument of type '(value: Config[K], previous?: Config[K]) => number' is not assignable to parameter of type '((value: boolean, previous?: boolean) => any) & ((value: string, previous?: string) => any)'.
    // Type '(value: Config[K], previous?: Config[K]) => number' is not assignable to type '(value: boolean, previous?: boolean) => any'.
    //  Types of parameters 'value' and 'value' are incompatible.
    //    Type 'boolean' is not assignable to type 'Config[K]'
    return _configuration[name];
}

function f(_configuration: Partial<Config>, settings: Partial<Config>) {
    let attr: keyof typeof settings;
    for(attr in settings) {
        let previous = _configuration[attr];
        let value = settings[attr];
        _configuration[attr] = value; // error on _configuration[attr]
        // Type 'string | boolean' is not assignable to type 'never'. Type 'string' is not assignable to type 'never'.
        _configTrackers[attr][0](value, previous); // error on value
        // Argument of type 'string | boolean' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.
    }
}

The first one is on this line _configTrackers[name]!.push(cb), I would like typescript to understand that it is possible. I tried several types for _configTrackers but I couldn't fund any that worked.

The second one is at _configuration[attr] = value;. I guess Typescript doesn't get that previous and value have the same type ... I have a weird error about "never" that I don't really get. How could I get Typescript to this assignement?

How could I get this code to typecheck without "hacks" like as any?

Upvotes: 0

Views: 96

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42248

What is causing the errors in function f is Typescript's failure to grasp that the the key which you are accessing from settings and the key which you are setting on _configuration are one and the same.

It assigns the type of both value and _configuration[attr] as string | boolean | undefined which is the union of all property values on Config with undefined included because settings uses Partial. Even though these two variables have the same type, one is not assignable to the other because the union itself is not assignable to any individual member of the union.

We can fix the error on _configuration[attr] = value by using a generic K for the key. This causes value and previous to have the type Partial<Config>[K].

function f(_configuration: Partial<Config>, settings: Partial<Config>) {

    function process<K extends keyof Config>(key: K) {
        let previous = _configuration[key];
        let value = settings[key];
        _configuration[key] = value; // success!
        const trackers = _configTrackers[key];
        if ( trackers ) {
            trackers[0](value, previous); // error
        }
    }
    
    let attr: keyof typeof settings;
    for(attr in settings) {
        process(attr);
    }
}

But we still have problems with the _configTrackers. Callbacks are rough. The tracker still ends up with the union of two functions. It is impossible to call this union because your arguments would need to be assignable to both branches and that is impossible for our two value types boolean and string.

const trackers: Partial<{
    autoStart: ((value: boolean, previous?: boolean | undefined) => any)[];
    baseUrl: ((value: string, previous?: string | undefined) => any)[];
}>[K]

We lose the K when guarding against undefined and it becomes

const trackers: ((value: boolean, previous?: boolean | undefined) => any)[] | ((value: string, previous?: string | undefined) => any)[]

Unless someone has a better suggestion, I think that you will have to make an assertion.

Upvotes: 1

Related Questions