Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
719 views
in Technique[技术] by (71.8m points)

typescript - Why can I index by string to get a property value but not to set it?

I was trying to solve this other question's problem which involves iterating through two objects of the same unknown type and adding together their number-typed properties without type assertions (if possible).

I got close (I thought I had it down to one type assertion) but I'm flummoxed by something I ran into: Why in the following can I use key to index result and currentValue when getting the value of a property, but not when setting the value of a property?

const a = {x:1, y:1, z:1};
const b = {x:2, y:2, z:2};

function reduceObjects<T extends {[key: string]: any}>(currentValue: T, previousValue: T): T {
    const result = Object.assign({}, previousValue) as T;

    for (const key of Object.keys(result)) {
        const prev = result[key];
        //           ^^^^^^^^^^^??????????????????????????????? why is this okay...
        const curr = currentValue[key];
        if (typeof prev === "number" && typeof curr === "number") {
            result[key] = prev + curr;
        //  ^^^^^^^^^^^???????????????????????????????????????? ...but this isn't?
        //                 "Type 'string' cannot be used to index type 'T'.(2536)"
        } else if (typeof prev === "string" && typeof curr === "string") {
            result[key] = prev + curr;
        //  ^^^^^^^^^^^ (same error here of course)
        }
    }

    return result;
}

const c = reduceObjects(a, b);

Playground link

My question is why is the error there. I'm not really trying to fix it (though if you can fix the above?— probably with a different approach entirely?— I do recommend you post an answer to the other question ??). I want to understand why it matters whether I'm getting or setting the property.

Titian Cernicova Dragomir pointed out that this changed between v3.3 (where it worked) and v3.5 onward (where it stopped working).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I'm feeling as a cheater because I used @aleksxor no-type-safe-way link. Sorry for that, I just thought that it worth some explanation.

I believe this argument is pretty good:

This is not an error.

T = { hello : "bye" }

Now, your assignment,

map["hello"] = "hi there"

Is unsound

Consider the following example:

let index: { [key: string]: any } = {}

let immutable = {
    a: 'a'
} as const

let record: Record<'a', 1> = { a: 1 }

index = immutable // ok
index = record // ok

const foo = (obj: { [key: string]: any }) => {
    obj['sdf'] = 2

    return obj
}

const result1 = foo(immutable) //  safe, see return type 
const result2 = foo(record) // safe , see return type

It works, because TS does not try to infer obj from generic argument. We can use any string we want as index.

Let's go back to our problem, now, TS tries to infer type of object

const foo = <T extends { [key: string]: any }>(obj: T) => {
    obj['sdf'] = 2 // error
}

{[key:string]:any} is too wide.

Examples of unsoundness:

let index: { [key: string]: any } = {}

let immutable = {
    a: 'a'
} as const

let record: Record<'a', 1> = { a: 1 }

index = immutable // ok
index = record // ok

const foo = <T extends { [key: string]: any }>(obj: T) => {
    obj['sdf'] = 2

    return obj
}

const result1 = foo(immutable) //  unsound, see return type 
const result2 = foo(record) // unsound , see return type

Hence, while you can get value by key, it is safe to disallow mutations by key.

Btw, TS does not play well with mutations, because it can't track them. Here you have another one good example why it is better to avoid mutations

If TypeScript didn't know the key exists in T, result[key] should be an error regardless of reading or writing. I get why writing may be unsound when reading isn't, I think that first sentence is just a tangent.

If you read property - it can not affect T object and you can return T without any problems.

const foo = <T extends { [key: string]: any }>(obj: T) => {
    const readOperation = obj['sdf']
    // object still has T type
    return obj
}

But if you mutate it:

const foo = <T extends { [key: string]: any }>(obj: T) => {
    obj['sdf'] = 2
    // this is not our good old `T` anymore,
    return obj
}

TS can't figure out the return type of function. Type signature is

const foo: <T extends {
    [key: string]: any;
}>(obj: T) => T

But it is not T anymore, since it mutated and because TS does not track mutations - it is unsafe to do.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...