typescripttypes

Why is setting a dynamic key unsound in typescript


Context / Reproduction

type Example = {
    a: string,
    b: number
}


let value: Example = {
    a: 'hello',
    b: 10,
}

// This IIFE is because typescript will narrow the type if assigned directly like
// const k: keyof Example = 'a'
const k = ((): keyof Example => 'a')()

value = {
    ...value,
    // Obviously this is unsound
    [k]: 5,
}

This passes even in strict mode. Playground link.

Question

Why is typescript unsound here? I want to understand the overarching unsoundness so I can avoid using patterns like this that will not be typesafe. I would also appreciate any relevant documentation or GitHub issues.


Solution

  • This is a known issue with TypeScript, documented at microsoft/TypeScript#38663. If you have a computed property name whose type is a union, then TypeScript will widen that type all the way to string (see microsoft/TypeScript#13948), and then more or less just ignores the property entirely. That is, it's even worse than possibly assigning a number to a, it could assign anything to a, including false (see this playground link).

    It's not currently classified as a bug, probably because such unsoundnesses with property writes permeate the language:

    // widen the type of value, this is considered "safe"
    const myVal: { [k: string]: string | number } = value;
    
    // but oops:
    myVal.b = "oopsieDoodle";
    

    Fixing such unsoundness would make things safer, but arguably a lot more annoying to use, as evidenced by the many complaints about microsoft/TypeScript#30769. TypeScript is unsound, largely in places where the TS team thinks the cure is worse than the disease. See TypeScript Language Design Non-Goal #3 and TS team comments in microsoft/TypeScript#9825.