Reputation: 2807
I need a sort of setProperty
function that allows setting properties on an object, but limiting them to properties of type string
.
It seems like I've figured out how to limit the allowed keys to the ones that have a value type of K
, but TypeScript doesn't seem to recognize that entirely. Here's what I have:
type KeysMatching<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
function bindChange<C, K extends KeysMatching<C, string>>(
container: C,
key: K,
): (ev: KeyboardEvent) => void {
return (ev: KeyboardEvent) =>
// @ts-ignore how do we tell typescript container[key] is a string?
(container[key] = (ev.target! as HTMLInputElement).value);
}
const sample = {
foo: 'bar',
baz: 42,
};
// this is correctly allowed
bindChange(sample, 'foo');
// this is correctly rejected
//bindChange(sample, 'baz');
What I'm trying to figure out is if there is a better approach that can eliminate the need for the @ts-ignore
in there.
Upvotes: 1
Views: 602
Reputation: 249636
In all fairness, the function is not 100% type safe, there is no way to ensure container[key]
is not a subtype of string
. The constraints are only a minimal requirement, the actual types can be more specific. Consider this:
const sample = {
foo: 'bar',
baz: 42,
} as const
// this is correctly allowed
bindChange(sample, 'foo'); // but 'foo' is of type 'bar' but is assigned a string
The problem is that in general Ts does not really do a lot of reasoning about conditional types that still have unresolved type parameters. So a lot of times generic function implementations will end up with type assertions. Don't worry about it too much IMO.
Since there is no way to make sure all properties are exactly string, the only solution, if you consider this corner case as not important, is to use a type assertion:
function bindChange<C, K extends KeysMatching<C, string>>(
container: C,
key: K,
): (ev: KeyboardEvent) => void {
return (ev: KeyboardEvent) =>
(container[key] = (ev.target! as HTMLInputElement).value as unknown as C[K]);
}
There is also this version, which does get around the issue, but looses intelisense for the function call, which is more important in my opinion:
type KeysMatching<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
function bindChange<C extends Record<K, string>, K extends PropertyKey>(
container: C,
key: K,
): (ev: KeyboardEvent) => void {
return (ev: KeyboardEvent) =>
(container[key] = (ev.target! as HTMLInputElement).value as unknown as C[K]);
}
const sample = {
foo: 'bar',
baz: 42,
};
// this is correctly allowed
bindChange(sample, 'foo');
Upvotes: 1