Please see the following example code:
type TestKey = 'a' | 'b' | 'c';
function printTestValue(key: TestKey, value: string) {
console.log(key, value);
}
{
// case 1
const testValues: { [key: TestKey]: string } = { // TS1337 on key
a: 'A',
b: 'B',
c: 'C',
};
for (const key in testValues)
if (Object.hasOwn(testValues, key))
printTestValue(key, testValues[key]); // TS2345 on key, TS7053 in testValues[key]
}
{
// case 2
const testValues: { [key in TestKey]: string } = {
a: 'A',
b: 'B',
c: 'C',
};
for (const key in testValues)
if (Object.hasOwn(testValues, key))
printTestValue(key, testValues[key]); // TS2345 on key, TS7053 in testValues[key]
}
{
// case 3, trying to remove key 'c'
const testValues: { [key in TestKey]: string } = { // TS2741 on testValues
a: 'A',
b: 'B',
};
for (const key in testValues)
if (Object.hasOwn(testValues, key))
printTestValue(key, testValues[key]); // TS2345 on key, TS7053 in testValues[key]
}
{
// case 4
const testValues: { [key in TestKey]?: string } = {
a: 'A',
b: 'B',
};
for (const key in testValues)
if (Object.hasOwn(testValues, key))
printTestValue(key, testValues[key]); // TS2345 on key, TS7053 in testValues[key]
}
Here, what I'm trying to accomplish is as follows:
- restrict the value of 'key' argument of
printTestValue()
function. - restrict the value of the keys of the
testValues
object, so that it's key-value pair can be safely used forprintTestValue()
function, while it does not have to have all the keys specified inTestKey
type.
But each trial (denoted as case 1, 2, 3, 4) has error(s) (shown in comments). How can I implement an error-free version?
CodePudding user response:
You approach of using a mapped type is the correct one.
An object type in Typescript does not guarantee the absence of extra properties on the object at runtime. Any subtype (in effect a type with more properties) can be assigned to a base type. So this code would be valid:
type TestKey = 'a' | 'b' | 'c';
function printTestValue(key: TestKey, value: string) {
console.log(key, value);
}
const someValue = { a: "A", b: "B", c: "C", d: 1 }
const testValues: { [key in TestKey]: string } = someValue; // ok typeof somevelu is a subtype
for (const key in testValues) {
if (Object.hasOwn(testValues, key)) {
// At runtime key will be "d" and testValues[key] will be a number
printTestValue(key, testValues[key]);
}
}
So this is why when using for..in
or Object.keys
we get string
not keyof testValues
.
You can use a type assertion if you want to avoid this compiler error: printTestValue(key as TestKey, testValues[key as TestKey]);
(Playground Link)
If you want to be safe from base type aliasing you can check exhaustively for the keys, make sure you only deal with expected keys and ignore (or error) on unexpected keys:
const testKeys = ["a", "b", "c"] as const
type TestKey = typeof testKeys[number]
function isTestKey(key: string): key is TestKey {
return testKeys.includes(key as TestKey)
}
function printTestValue(key: TestKey, value: string) {
console.log(key, value);
}
const someValue = { a: "A", b: "B", c: "C", d: 1 }
const testValues: { [key in TestKey]: string } = someValue;
for (const key in testValues) {
if (Object.hasOwn(testValues, key) && isTestKey(key)) {
printTestValue(key, testValues[key]);
} else {
// throw new Error("Unexpected key")
}
}
CodePudding user response:
First of all I would create an array of all possible keys that we can also access on runtime:
const testKeys = ['a', 'b', 'c'] as const
type TestKeys = typeof testKeys[number]
We can also create a type guard to check if a string
is actually of type TestKeys
:
const keyIsTestKey = (key: string): key is TestKeys => {
return testKeys.includes(key as TestKeys)
}
When we create an object like testValues
you may use the following type:
const testValues: Partial<Record<TestKeys, string>> = {
a: 'A',
b: 'B'
}
This specifies that only TestKeys
values can be used as keys but you don't have to specify all keys.
A bigger problem is the for ... in
loop. In TypeScript the type of the key
in for ... in
loops is always string
. But we can use the type guard here to make sure that key
is actually of type TestKeys
.
for (const key in testValues){
if (keyIsTestKey(key)){
/* ... */
}
}
Now for the last part: Since key
is now of type 'a' | 'b' | 'c'
but testValues
must not have all keys testValues[key]
may be undefined
...
I would use the Non-null assertion operator !
here since we are sure at this point that this can't be undefined.
for (const key in testValues){
if (keyIsTestKey(key)){
if (Object.hasOwn(testValues, key))
printTestValue(key, testValues[key]!)
}
}