Skip to content

Type matching inside of nested mapped type causes error on assignment, but otherwise TS knows the right type #51117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MichaelTontchev opened this issue Oct 9, 2022 · 2 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@MichaelTontchev
Copy link

Bug Report

πŸ”Ž Search Terms

typescript two level mapped type inference error
Possible match? #23897

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about mapped types

⏯ Playground Link

https://www.typescriptlang.org/play?#code/C4TwDgpgBA6glsAFgBQPYGd1wEYBsLIBOqkhoA0gPIBmAKuBLQDzlQQAewEAdgCbpR0wQnG4BzADRRaAPigBeKAG8AUFHVQA2siiio5ALoB+AFzSA3CoC+llQGNU3IVDsBDO4ggARV8FcKoJloAZQgHPgAZCAA3CFw2Th5+WAQUDCw8AmJSChp6SGZyKSERcRkpVg4uPgES0TEZAAo1DQBrCBAaAGF3TwBJblDw3ijY3DMilvU7AFdCQh5gHo8IMwAlMNRCXiY68SkQzciYuJkASimoM2jUOF4FOVUNKAB6F6gAUXmts3zoAHI9mJ-roBNxUMAoK5MHAxNxXJkoMBUEiGFB-odhqM4ppDP8AHSXWbzRbLTyaQGoAC2ED6vH+Bk07U61DJtMGRxGJ1wBgC-1c-1sz3Czg4cCE9QCxIW3CWvQgFPQ1Np9MZzO68oGQ0cXLGBiFGjeUGCMzsdggmGoM1wuBAUAWrlwcAAXhakYhfKjIKCoYJhPVLmKJeIjPiPK5CABBYCNABMZ0sNiAA

πŸ’» Code

type WithPossiblePropertyKOfTypeT<K extends string, T> = {
    [P in K]?: T;
};

const cacheData = <TSecondLevel extends WithPossiblePropertyKOfTypeT<K, string>, K extends string>(
    keyOfCacheInSecondLevel: K,
    currentCache: Record<string, TSecondLevel>)
    : void => {
    // Error: Type 'string' is not assignable to type 'TSecondLevel[K]'.
    currentCache['someId'][keyOfCacheInSecondLevel] = 'a';

    const existing = currentCache['someId'][keyOfCacheInSecondLevel];

    // Successfully realizes that the type is a string
    existing?.charAt(2);
};

πŸ™ Actual behavior

Type 'string' is not assignable to type 'TSecondLevel[K]'.

πŸ™‚ Expected behavior

No error.

The type I'm using seems a tiny bit contrived, but the scenario is that I'm inside of a reducer that is later reduced with other reducers to produce a slice reducer.

The overall state slice looks like the following:

users: {
    'someUserId': {
        someIrrelevantUserProperty: 1;
        cachedUserProperty: {/* some object*/}
    }
    // ... more users here
}

The reducer I'm working on takes the entire state slice users and the key of that cached property in the second level and it needs to specifically handle updating the same property, (in the above example, cachedUserProperty). Other reducers handle updating the other properties inside the specific user, and the several reducers are reduced together to produce the slice reducer. My reducer is generically written, because I have several such structures in my state, with different types and keys in the second level. (Actually the reducer is produced by a reducer producer to achieve this, but that's too much detail).

The type that you see in my repro, WithPossiblePropertyKOfTypeT, allows me to model the fact that the second level of my data (ie, users[string]) has a property with key K of type T | undefined.

In the repro, notice that when I try to assign string to that property, it errors out. But TS knows that that is a string, because it gives no errors when I try to access string functions on an already-existing value in that property.

The full repro has the type of that property be a more complex object, rather than string, and TS even knows the types inside that object if I assign it to a variable and then try to access other properties inside of it.

So TS knows what the type is, but it's erroring out when trying to equate it to TSecondLevel[K].

@jcalz
Copy link
Contributor

jcalz commented Oct 9, 2022

Conceptually this is related to #30728 and #48992, but the particular example code you have here is working as intended. The error is a good one.


The caller of the function chooses the TSecondLevel type argument, not the implementer. It is possible for the values of this type argument to be narrower than string. The compiler knows that TSecondLevel[K] extends string | undefined, but it can't be sure that (string | undefined) extends TSecondLevel[K].

Consider this code:

interface Shirt {
    size: "S" | "M" | "L";
    price: number;
}
const sizeName = { S: "small", M: "medium", L: "large" };

const shirt: Shirt = { size: "S", price: 20 };
const shirts = {
    someId: shirt
};
console.log(sizeName[shirt.size].toUpperCase()) // "SMALL"

Now this happens:

cacheData("size", shirts);
// cacheData<Shirt, "size">(...);
console.log(sizeName[shirt.size].toUpperCase()) // πŸ’₯ RUNTIME ERROR

The runtime error occurs because you've changed shirt.size to "a" inside cacheData(), and that was a problem precisely because, in this case, TSecondLevel[K] is Shirt["size"] which is "S" | "M" | "L", and you shouldn't assign an "a" to that.

Playground link to code

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Oct 10, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants