I want to have a function that grabs something from an object; one of the function arguments is used as a key for that object. It seems like when an object has a dynamic property, keyof will not limit what a key is based on that property, but accepts all strings.
For example (a very simple one), this makes an object extractor that lets a user extract a value from an internal object by passing in a key of that object:
function makeObjectExtractor(
keyA: string,
) {
const toExtractFrom = {
// keyA is dynamic; as a result, keyof allows for any string or number
[keyA]: 4,
keyB: 5
} satisfies Record<string, number>;
function getIncrementedVal(param: keyof typeof toExtractFrom) {
return toExtractFrom[param] 1;
}
return getIncrementedVal;
}
const extractor = makeObjectExtractor('g');
// this should be flagged as a typescript error, but it isn't because keyA is dynamic
extractor('asdfasdf');
Is there a way for typescript to know here that only g
and keyB
are the allowed keys?
If I make all the keys hardcoded, this issue goes away, and keyof can only be a key of that object:
function makeObjectExtractor(
) {
const toExtractFrom = {
// keyA is dynamic; as a result, keyof allows for any string or number
b: 4,
keyB: 5
} satisfies Record<string, number>;
function getIncrementedVal(param: keyof typeof toExtractFrom) {
return toExtractFrom[param] 1;
}
return getIncrementedVal;
}
const extractor = makeObjectExtractor();
// this now properly raises an error:
extractor('asdfasdf');
// this does not raise an error, which is expected:
extractor('b');
Now keyof correctly limits to the values "keyA" | "keyB"
Is there a way to get keyof
to work with dynamic keys?
CodePudding user response:
function makeObjectExtractor(
) {
const toExtractFrom = {
// keyA is no longer dynamic; keyof works as expected now
keyA: 'b',
keyB: 'c'
}
type ExtractType = typeof toExtractFrom;
function extract(a: keyof ExtractType | number): string {
return toExtractFrom[a];
}
return extract;
}
Like this? But if you do | string
type of a
will become string
CodePudding user response:
I'm not sure if there is a way to make that work without as
. You'll have to have one as
somewhere, so it could be in a function to not worry about typing
function recordFromEntries<K extends PropertyKey, V>(entries: [K, V][]): Record<K, V> {
return Object.fromEntries(entries) as Record<K, V>;
}
let x = recordFromEntries
function makeObjectExtractor<KeyA extends string>(
keyA: KeyA,
) {
const toExtractFrom = {
keyB: 5,
...recordFromEntries([
[keyA, 4]
]),
} satisfies Record<string, number>;
function getIncrementedVal(param: keyof typeof toExtractFrom): number {
return toExtractFrom[param] 1;
}
return getIncrementedVal;
}
const extractor = makeObjectExtractor('g');
// this should be flagged as a typescript error, but it isn't because keyA is dynamic
extractor('asdfasdf');
CodePudding user response:
You would have to make the type of keyA
generic. Otherwise, TypeScript will see the type of keyA
only as string
and not the literal type of the value you pass into the function.
function makeObjectExtractor<KeyA extends string>(
keyA: KeyA,
) {
const toExtractFrom = {
...{ [keyA]: 4 } as Record<KeyA, number>,
keyB: 5
} satisfies Record<string, number>
function getIncrementedVal(param: keyof typeof toExtractFrom) {
return toExtractFrom[param] 1;
}
return getIncrementedVal;
}
When using computed property names, TypeScript will always add an index signature to the type. To prevent this, we can use the spread operator to add an object with the key keyA
to toExtractFrom
. But we assert that the object is of type Record<KeyA, number>
.
This will lead to the following result:
const extractor = makeObjectExtractor('g');
// valid
extractor("keyB")
extractor("g")
// invalid
extractor('asdfasdf');