2

The closest one I had found Typescript fails to check nested object on function return type but in my case doesn't involve any return. Am I missing anything fundamental? Dynamic key or how typescript handle the shape of object with extra properties?

What's the correct way to handle this problem so that I can make sure the definition of SpecKey param to be correct. Such that I need to delete random otherwise a ts error will raise up.

Full code:

type TestObj = {
    valid?: string;
}

type SpecKey = "valid" | "random";

const objA: TestObj = {
    valid: "something",
}

function testFunc(key: SpecKey) {
    const objC: TestObj = {
        ...objA,
// this is ok even the SpecKey could be "invalid" during the runtime 
// even we have the object type annotated
        [key]: 123123213,
    }
}

Playground: https://www.typescriptlang.org/play?ts=5.2.2&ssl=23&ssc=2&pln=1&pc=1#code/C4TwDgpgBAKhDOwDyAjAVlAvFA3gKCkKgDcBDAGwEsATAfgC4pEAnSgOwHMBuPAXzzyhIUAMqQAxgGkIILFABEZKtXlQAPguak21APYBbeTzzjdbRFF3oAgoziJUGbPiIkKNRvPgGIwABbsHPIANHwCpubATBLSIIxiEFIycgCypP4AdFo6BgAUAJRQADxQAAwZAKxQtApKNKqe2XqGxhEWVmgAQnYIyOhyLkQZwx3Woa4A2vAxMgC6jACMAEwAzMsrSwsrofx4AGYArmziwJRmUMC9AGJH4rkA1jLxMyCFg4RtUR0Awj0O-c4CK4hiMbONgYQJo8QPMoOt1pttkDCPxeEA

Yunhai
  • 1,283
  • 1
  • 11
  • 26
  • There are multiple issues and you should probably [edit] so that only one is at play. Example: outside the function `specKey` undergoes [assignment narrowing](//www.typescriptlang.org/docs/handbook/2/narrowing.html#assignments) so its apparent type is just `"random"` and not `SpecKey`. If `specKey` really can be either union member, you'll see the behavior switch so that *both* object literals are accepted, see [this playground link](//tsplay.dev/WzDBQN). Now the behavior is consistent, but possibly still confusing to you? If so, please [edit] to remove the distraction of assignment narrowing. – jcalz Aug 29 '23 at 00:59
  • @jcalz, thx, I think you are correct, before I edit the problem and remove the first example and leave the dynamic key only, what could be the possible solution to tackle this problem for the dynamic key in typescript. Does it mean I need to explicitly do something like `if (key === "valid") ` to guard against the key before object merge? Is there anything I can do to create error hint in case if I make a typo like `type SpecKey = "validtypo"?` for example, otherwise it seems like ts doesn't hint anything here and I always need to keep the `if` check in my mind. – Yunhai Aug 29 '23 at 01:29
  • @jcalz the question is edited and fixed. hopefully it can still convey my original question accurately. Though it seems like my question then could be intended by the design of typescript. Still wonder is there anything I can do about it. – Yunhai Aug 29 '23 at 01:46
  • [Excess property checks](https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks) are considered more of a linter feature than a type safety issue (see [this q](https://stackoverflow.com/q/69647392/2887218)) and don't always get enforced, so it's *sort of* intended. The issue *here* is discussed at [ms/TS#36920](https://github.com/microsoft/TypeScript/issues/36920)... it seems like it was originally considered a bug, but now it's being treated as a feature request. Does that fully address the question (If so I'll write up an answer) or am I missing something? – jcalz Aug 29 '23 at 02:02
  • @jcalz yes, free feel to do so then I can mark your answer as solution. Still process some of the links you provided but can't clearly come out the workaround for such a "simple case". – Yunhai Aug 29 '23 at 02:26

1 Answers1

1

There are a few open issues regarding the treatment of computed property keys in TypeScript that would need to be addressed to give you the expected behavior. One is microsoft/TypeScript#36920 in which computed properties are never treated as excess properties, and another is microsoft/TypeScript#38663 in which computed keys of union type end up being ignored entirely if they partially conflict with known property keys. Both of these issues are considered feature requests (and not bugs, even though the latter one leads to unsoundness).

Excess property checks are more of a linter feature than a type safety feature, and don't get applied everywhere. In some sense they are at odds with a fundamental property of the TypeScript type system: when you extend a type by adding extra properties, the new type is still structurally compatible with the old type. Excess properties therefore must be allowed in many circumstances. They are only considered errors in specific scenarios involving object literals, and apparently computed properties are not one such scenario. Maybe they should be, as microsoft/TypeScript#36920 suggests, but they aren't. So in your code, the possibility that random is added as a key doesn't raise any warnings. Oh well. That explains why { valid?: string, random: number } is allowed.

Potentially more problematic is the fact that computed property keys of a union type get widened all the way to a string index signature (see microsoft/TypeScript#13948), and then silently dropped when combined with other properties. So the type of const objB = { ...objA, [specKey]: 123123213 } is just { valid?: string | undefined; } and not the expected { valid?: string, random: number } | { valid: number }. We know that { valid?: string, random: number } is structurally compatible with TestObj, but { valid: number } is certainly not. Nowhere are we warned that we might be assigning a number to a property that expects a string. That's what microsoft/TypeScript#38663 suggests. If you want to see that happen, you might want to go to that issue and give it a . Until and unless that's implemented, you'll need to work around it.


One workaround I tend to use for the index signature issue is to wrap computed property key creation in a helper function that gives the typings I expect. See this comment on microsoft/TypeScript#13948. Like this, for instance:

function kv<K extends PropertyKey, V>(k: K, v: V): { [P in K]: { [Q in P]: V } }[K] {
    return { [k]: v } as any
}

Then instead of {[k]: v} in a literal, I write {...kv(k, v)}:

function testFunc(key: SpecKey) {
    const objC: TestObj = { ...objA, ...kv(key, 123123123) }; // error!
    // Type '{ valid: number; } | { random: number; valid?: string | undefined; }' 
    // is not assignable to type 'TestObj'
}

Now you get the error you expect, specifically that you are trying to assign a value of type { valid: number; } | { random: number; valid?: string } to a variable of type { valid?: string }, which is not allowed, because number is not a string.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • thx for the long answer, I checked the playground link and when I used `...kv(key, "a string")`, it still bypasses the typescript check, isn't the purpose is to flag the `random` key, am I missing anything here? I got the return type as `{ valid: string ;} | { random: string; }`, which should be "invalid" I guess. I'm also coming through the lib `type-fest` has a `Exact` helper, not sure if it can tackle that issue. – Yunhai Aug 29 '23 at 03:52
  • 1
    The purpose of `kv` is to deal with the index signature issue, not the excess property issue. Excess properties are not possible to prohibit in general, although you might get somewhere close with an `Exact` helper. See [this q/a](https://stackoverflow.com/q/69647392/2887218) for more info. – jcalz Aug 29 '23 at 13:01