setStore TypeScript Issue

I have a object store that has an interface like this:
export interface config {
object1?: type1;
object2?: type2;
name?: string;
...
id?: number;
}
export interface config {
object1?: type1;
object2?: type2;
name?: string;
...
id?: number;
}
type1 looks like this:
export interface type1 {
channel?: number;
role?: string;
value?: number;
present?: boolean;
}
export interface type1 {
channel?: number;
role?: string;
value?: number;
present?: boolean;
}
type2 looks like this:
export interface type2 {
channel?: number;
role?: string;
value?: number;
invert?: boolean;
}
export interface type2 {
channel?: number;
role?: string;
value?: number;
invert?: boolean;
}
Now I have a components where I pass in the following props struct
{id: Part<config, keyof config>, setConfig: SetStoreFunction<config>}
{id: Part<config, keyof config>, setConfig: SetStoreFunction<config>}
Within the component I make calls like:
props.setConfig(props.id, "role", "example")
props.setConfig(props.id, "role", "example")
(where props.id = "object1" or "object2") These do not create any typescript error. When I make a call like this:
props.setConfig(props.id, "invert", "example")
props.setConfig(props.id, "invert", "example")
(where props.id = "object2") I get an error like this:
Argument of type '"invert"' is not assignable to parameter of type 'Part<type1| type2, "channel" | "role" | "value">
Argument of type '"invert"' is not assignable to parameter of type 'Part<type1| type2, "channel" | "role" | "value">
The code runs fine without error, so I feel like I am not actually using setStore incorrectly. Why doesn't TypeScript recognize "invert" as valid Part in this situation? Is there any way I can fix this without supressing the TypeScript error?
11 Replies
peerreynders
peerreynders6d ago
Part<type1| type2, "channel" | "role" | "value">
… suggests to me that problem is inside the Part<T,K> type- I'm noticing how the second union only lists types that are shared between type1 and type2; role is in that union, invert is not.
Pseudotronics
Pseudotronics5d ago
Yeah, I noticed that too. I do not however know enough to say if this issue is a bug or not. Perhaps i will make an issue on the repo and see what people have to say. here is a full example of the error:
Pseudotronics
Pseudotronics5d ago
I am thinking that I may have to narrow the Part<T,K> to the specific interface being passed through. I don't know how to do that though.
peerreynders
peerreynders5d ago
TypeScript needs more help to understand your types:
type KeysOfType<R, T> = Exclude<
{
[key in keyof R]: Exclude<R[key], undefined> extends T ? key : never;
}[keyof R],
undefined
>;

type Type1Key = KeysOfType<config, { present?: boolean }>;
type Type2Key = KeysOfType<config, { invert?: boolean }>;

const Type1Comp: Component<{
id: Type1Key;
subConfig: type1;
setConfig: SetStoreFunction<config>;
}> = (props) => {
const changeConfig = () => {
props.setConfig(props.id, 'channel', 9);
props.setConfig(props.id, 'role', 'changed');
props.setConfig(props.id, 'value', 50);
props.setConfig(props.id, 'present', !props.subConfig.present);
};

return (
<>
<div>This works ({props.subConfig.present ? 'true' : 'false'})</div>
<button onclick={changeConfig}>type1</button>
</>
);
};

const Type2Comp: Component<{
id: Type2Key;
subConfig: type2;
setConfig: SetStoreFunction<config>;
}> = (props) => {
const changeConfig = () => {
props.setConfig(props.id, 'channel', 9);
props.setConfig(props.id, 'role', 'changed');
props.setConfig(props.id, 'value', 50);
props.setConfig(props.id, 'invert', !props.subConfig.invert);
};

return (
<>
<div>So does this ({props.subConfig.invert ? 'true' : 'false'})</div>
<button onclick={changeConfig}>type2</button>
</>
);
};
type KeysOfType<R, T> = Exclude<
{
[key in keyof R]: Exclude<R[key], undefined> extends T ? key : never;
}[keyof R],
undefined
>;

type Type1Key = KeysOfType<config, { present?: boolean }>;
type Type2Key = KeysOfType<config, { invert?: boolean }>;

const Type1Comp: Component<{
id: Type1Key;
subConfig: type1;
setConfig: SetStoreFunction<config>;
}> = (props) => {
const changeConfig = () => {
props.setConfig(props.id, 'channel', 9);
props.setConfig(props.id, 'role', 'changed');
props.setConfig(props.id, 'value', 50);
props.setConfig(props.id, 'present', !props.subConfig.present);
};

return (
<>
<div>This works ({props.subConfig.present ? 'true' : 'false'})</div>
<button onclick={changeConfig}>type1</button>
</>
);
};

const Type2Comp: Component<{
id: Type2Key;
subConfig: type2;
setConfig: SetStoreFunction<config>;
}> = (props) => {
const changeConfig = () => {
props.setConfig(props.id, 'channel', 9);
props.setConfig(props.id, 'role', 'changed');
props.setConfig(props.id, 'value', 50);
props.setConfig(props.id, 'invert', !props.subConfig.invert);
};

return (
<>
<div>So does this ({props.subConfig.invert ? 'true' : 'false'})</div>
<button onclick={changeConfig}>type2</button>
</>
);
};
Always keep in mind that TypeScript is structurally typed, not nominally typed.
Pseudotronics
Pseudotronics5d ago
I guess I don't understand why doing something like this:
type Type1Key = KeysOfType<config, type1>;
type Type1Key = KeysOfType<config, type1>;
Doesn't work. That is generally the behavior I want, but I don't know how to write KeysOfType to make that happen, or even if it is possible. Interesting. If I remove the optional attribute from the interfaces I can have it like that.
peerreynders
peerreynders5d ago
Perhaps you would be more comfortable with
type Type1Key = KeysOfType<config, Pick<type1, 'present'>>;
type Type2Key = KeysOfType<config, Pick<type2, 'invert'>>;
type Type1Key = KeysOfType<config, Pick<type1, 'present'>>;
type Type2Key = KeysOfType<config, Pick<type2, 'invert'>>;
With conditional types extends simply means:
the type on the left of is assignable to the one on the right.
if you look at your definition …
interface type1 {
channel?: number;
role?: string;
value?: number;
present?: boolean;
}
interface type1 {
channel?: number;
role?: string;
value?: number;
present?: boolean;
}
… where you have elected to make all members optional. Consequently:
type X = {} extends type1 ? 'yes' : 'no;' // type X = 'yes'
type Y = type2 extends type1 ? 'yes' : 'no;' // type Y = 'yes'
type Z = { channel?: number; invert?: boolean } extends type1 ? 'yes' : 'no;' // type Z = 'yes'
type A = { invert?: boolean } extends type1 ? 'yes' : 'no;' // type A = 'no'
type X = {} extends type1 ? 'yes' : 'no;' // type X = 'yes'
type Y = type2 extends type1 ? 'yes' : 'no;' // type Y = 'yes'
type Z = { channel?: number; invert?: boolean } extends type1 ? 'yes' : 'no;' // type Z = 'yes'
type A = { invert?: boolean } extends type1 ? 'yes' : 'no;' // type A = 'no'
Perhaps you would be shocked to realize:
let t1: type1 = {
channel: 9,
role: 'changed',
value: 50,
present: false,
};

const t2: type2 = {
channel: 9,
role: 'changed',
value: 50,
invert: false,
};

t1 = t2; // no error
let t1: type1 = {
channel: 9,
role: 'changed',
value: 50,
present: false,
};

const t2: type2 = {
channel: 9,
role: 'changed',
value: 50,
invert: false,
};

t1 = t2; // no error
Object assignment is not subject to excess property checking. Hence my warning about structural typing.
Documentation - Conditional Types
Create types which act like if statements in the type system.
peerreynders
peerreynders5d ago
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
Pseudotronics
Pseudotronics5d ago
Ah that makes sense. I think I may need to take a different approach to the structure of my project to avoid this. Thanks for all the help.
peerreynders
peerreynders5d ago
You may want to leverage discriminated unions.
Documentation - TypeScript for Functional Programmers
Learn TypeScript if you have a background in functional programming
Pseudotronics
Pseudotronics5d ago
It was simple enough to make the extends condition not be true for my application.
KeysOfType<config, type1>;
KeysOfType<config, type1>;
Works great after making the types be incompatible with one another. Thanks again for all of your insight.