Reputation: 4172
I have this situation:
const data: Record<string, string> = {
a: '110px',
b: '160px',
};
interface Props {
d?: keyof typeof data[];
}
const t = (d: Props) => 'hi' + d;
t(['a']) // here should be allowed only data keys (a and b)
I want to restrict the t()
arguments only to data
object keys. I tried as above but I get TS error:
Type 'string[]' has no properties in common with type 'Props'.(2559)
How to achieve this?
NOTE: I need to do this dynamically reading the keys of the object.
Demo: link
Upvotes: 1
Views: 601
Reputation: 1074048
Two answers for you:
If by "dynamically" you mean at runtime, no, that's not possible with TypeScript. TypeScript only works with compile-time information.
If by "dynamically" you mean it works even if you change the object literal that creates data
, you can do it with (keyof typeof data)[]
and allowing TypeScript to infer the type of data
(by removing the type annotation on it):
const data = { // <== Note no `Record<string, string>` on this
a: '110px',
b: '160px',
};
const t = (d: (keyof typeof data)[]) => 'hi' + d;
t(["a", "b"]); // <=== Works as expected
t(["x"]); // <=== Error as desired
If you change the object literal defining data
so that it (now) has a c
property, the definition of t
doesn't have to change (playground), it picks that up.
As captain-yossarian from Ukraine pointed out in a comment, starting with TypeScript 4.9, if you want to make sure that you can't accidentally add an entry to data
that doesn't have a string value (or even a more specific one, more in a minute), you can use the new satisfies
operator. That lets you enforce a limitation on the object's properties without broadening its type, like this:
const data = {
a: '110px',
b: '160px',
} satisfies Record<string, string>;
//^^^^^^^^^−−−−−− new TypeScript 4.9 operator
const t = (d: (keyof typeof data)[]) => 'hi' + d;
t(["a", "b"]); // <=== Works as expected
t(["x"]); // <=== Error as desired
When you do that, if you try to add c: 42
to data
, you'll get an error from TypeScript (link) because that would mean that data
's type didn't satisfy the constraint Record<string, string>
. But unlike as
, it won't narrow the type of data
, so our keyof typeof data
still sees only data
's actual property names as a union ("a" | "b"
), not just string
.
As he also pointed out, if you want to make sure all of your values in data
are in the form <number>px
, you can use a template literal type like Record<string, `${number}px`>
with satisfies
(link).
Upvotes: 4