Reputation: 15
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
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