In the TypeScript code snippet below, I need to assign from one object to another where both of them are Partial<InjectMap>. Here, my intuition says that typescript should be able to understand what is going on because at line (B), the type of key is typeof InjectMap. So, it should be able to assign values from input to output correctly.
export interface InjectMap {
    "A": "B",                          // line (A)
    "C": "D"
}
type InjectKey = keyof InjectMap;
const input: Partial<InjectMap> = {};
const output: Partial<InjectMap> = {};
const keys: InjectKey[] = []
for (let i = 0; i < keys.length; i++) {
    const key = keys[i]                // line (B)
    output[key] = input[key]           // line (C)  - Gives Error
}
Playground Link
But it gives the following error at line (C):
Type '"B" | "D" | undefined' is not assignable to type 'undefined'.
  Type '"B"' is not assignable to type 'undefined'.
Strangely, the error goes away if I comment line (A). Is this a shortcoming in TypeScript or am I missing something?
I don't think it is a bug, it is almost always unsafe to mutate the values and TS just tries to make it safe.
Let's start from InjectMap interface.
It is clear that you cant have illegal state like:
const illegal: InjectMap = {
    "A": "D", // expected B
    "C": "B" // expected D
}
This is important.
Let's proceed with our loop:
interface InjectMap {
    "A": "B",
    "C": "D"
}
type InjectKey = keyof InjectMap;
const input: Partial<InjectMap> = {};
const output: Partial<InjectMap> = {};
const keys: InjectKey[] = []
for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const inp = input[key] // "B" | "D" | undefined
    const out = output[key] // "B" | "D" | undefined
    output[key] = input[key]
    
}
Because key is dynamic, TS is unsure whether it B, D or undefined. I hope that you are agree with me, that in this place correct type of inp is "B" | "D" | undefined, it is expected behavior, because type system is static.
Since, input and output are not binded by key, TS wants to avoid illegal state. To make it clear, consider next example, which is equal to our
type KeyType_ = "B" | "D" | undefined
let keyB: KeyType_ = 'B';
let keyD: KeyType_ = 'D'
output[keyB] = input[keyD] // Boom, illegal state! Runtime error!
As you might have noticed, keyB and keyD have the same type but different values.
Same situation you have in your example, TS is unable to figure out the value it is able to figure out only type.
If you want to make TS happy, you should add condition statement or typeguard:
for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    if (key === 'A') {
        let out = output[key] // "B"
        let inp = input[key] //  "B"
        output[key] = input[key] // ok
    }
    if (key === 'C') {
        let out = output[key] // "D"
        let inp = input[key] //  "D"
        output[key] = input[key] // ok
    }
}
Please keep in mind, when you mutate your values, you loose type guaranties.
See this and this question about mutations.
Also this talk of Titian Dragomir Cernicova is pretty good.
Here you have an example of unsafe mutation, taken from @Titian 's talk:
type Type = {
    name: string
}
type SubTypeA = Type & {
    salary: string
}
type SubTypeB = Type & {
    car: boolean
}
type Extends<T, U> =
    T extends U ? true : false
let employee: SubTypeA = {
    name: 'John Doe',
    salary: '1000$'
}
let human: Type = {
    name: 'Morgan Freeman'
}
let director: SubTypeB = {
    name: 'Will',
    car: true
}
// same direction
type Covariance<T> = {
    box: T
}
let employeeInBox: Covariance<SubTypeA> = {
    box: employee
}
let humanInBox: Covariance<Type> = {
    box: human
}
// Mutation ob object property
let test: Covariance<Type> = employeeInBox
test.box = director // mutation of employeeInBox
const result_ = employeeInBox.box.salary // while result_ is undefined, it is infered a a string
// Mutation of Array
let array: Array<Type> = []
let employees = [employee]
array = employees
array.push(director)
const result = employees.map(elem => elem.salary) // while salary is [string, undefined], is is infered as a string[]
console.log({result_,result})
Playground
How to fix it ?
Please, let me know if it works for you:
export interface InjectMap {
    "A": "B",
    "C": "D"
}
const assign = <Input extends InjectMap, Output extends InjectMap>(
    input: Partial<Input>,
    output: Partial<Output>,
    keys: Array<keyof InjectMap>
) => keys.reduce((acc, elem) => ({
    ...acc,
    [elem]: input[elem]
}), output)
Playground
UPDATE
why does this analysis apply for [elem]: input[elem]? input[elem] can again be "B"|"D"|undefined and thus the compiler should give error again. But, here compiler is intelligent to know that the type of input[elem] applies for [elem]. What is the difference?
That is a very good question.
When you create new object with computed key, TS makes this object indexed by string, I mean, you can use any string prop you want
const computedProperty = (prop: keyof InjectMap) => {
    const result = {
        [prop]: 'some prop' //  { [x: string]: string; }
    }
    return result
}
It gives you more freedom but also provides a bit of unsafety.
With great power comes great responsibility
Because now, unfortunately, you can do this:
const assign = <Input extends InjectMap, Output extends InjectMap>(
    input: Partial<Input>,
    output: Partial<Output>,
    keys: Array<keyof InjectMap>
) => keys.reduce((acc, elem) => {  
    return {
        ...acc,
        [elem]: 1 // unsafe behavior
    }
}, output)
As you might have noticed, return type of assign function is Partial<Output>, which is not true.
Hence, in order to make it completely type safe you can use with typeguards, but I think it will be overcomplicating
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With