Home > Net >  TypeScript: How to restrict index signature parameter type's keys?
TypeScript: How to restrict index signature parameter type's keys?

Time:05-10

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 for printTestValue() function, while it does not have to have all the keys specified in TestKey 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]);
    }
}

Playground Link

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")
    }
}

Playground Link

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]!)
  }
}

Playground

  • Related