最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Type 'string | number' is not assignable to type 'never' - Stack Overflow

programmeradmin5浏览0评论

I want to dynamically map the keys and values from list into obj. However, TS gives me an error message:

Type 'string | number' is not assignable to type 'never'

where I have no idea about what's wrong. Below is the code snippet:

interface T {
    // unment the next line makes the error go away
    // [k: string]: any
    a: string;
    b?: string;
    c?: number;
}

const obj: T = {
    a: 'something',
};

const list: Array<{
    foo: keyof T;
    bar: string | number;
}> = [
    { foo: 'b', bar: 'str' },
    { foo: 'c', bar: 1 },
];

list.forEach(item => {
    const { foo, bar } = item;

    // The error message es from the next line
    obj[foo] = bar;
});

I notice that if I include the typing [k: string]: any into the interface T, the error message goes away.

However, I am reluctant to do that because I can then add other key/value pairs into the obj, such as obj.d = 'error' without TS warning me.

Also, I am curious about why TS would gives me this error message and what is the type never thing is all about.

For the tsconfig.json, I am using the default values by running tsc --init with version 3.5.1

Thank you.

I want to dynamically map the keys and values from list into obj. However, TS gives me an error message:

Type 'string | number' is not assignable to type 'never'

where I have no idea about what's wrong. Below is the code snippet:

interface T {
    // unment the next line makes the error go away
    // [k: string]: any
    a: string;
    b?: string;
    c?: number;
}

const obj: T = {
    a: 'something',
};

const list: Array<{
    foo: keyof T;
    bar: string | number;
}> = [
    { foo: 'b', bar: 'str' },
    { foo: 'c', bar: 1 },
];

list.forEach(item => {
    const { foo, bar } = item;

    // The error message es from the next line
    obj[foo] = bar;
});

I notice that if I include the typing [k: string]: any into the interface T, the error message goes away.

However, I am reluctant to do that because I can then add other key/value pairs into the obj, such as obj.d = 'error' without TS warning me.

Also, I am curious about why TS would gives me this error message and what is the type never thing is all about.

For the tsconfig.json, I am using the default values by running tsc --init with version 3.5.1

Thank you.

Share Improve this question asked Dec 14, 2019 at 19:08 Ray ChanRay Chan 1,1809 silver badges20 bronze badges 1
  • 2 It's this change. It's worried that you're going to write {foo: "a", bar: 1}. – jcalz Commented Dec 14, 2019 at 19:37
Add a ment  | 

1 Answer 1

Reset to default 8

TypeScript 3.5 closed a loophole whereby index-access writes on unions of keys were not being properly checked. If I have an object obj of type T, and a key foo of generic type keyof T, then although you can safely read a property of type T[keyof T] from obj[foo],like const baz: T[keyof T] = obj[foo], it might not be safe to write such a property, like const bar: T[keyof T] = ...; obj[foo] = bar; In your code, foo might be "a" and bar might be 1, and that would be unsafe to write.

The way the loophole got closed: if I read a value from a union of keys, it bees a union of the property types, as before. but if I write a value to a union of keys, it bees an intersection of property types. So say I have an object o of type {a: string | number, b: number | boolean} and I want to write something to o[Math.random()<0.5 ? "a" : "b"]... what is safe to write? Only something which works for both o.a and o.b... that is, (string | number) & (number | boolean), which (when you fiddle with distributing unions across intersections and reducing) bees just number. You can only safely write a number.

In your case, though, the intersection is string & string & number. And unfortunately, there's no value which is both a string and a number... so that gets reduced to never. Oops.


To fix this case I'd probably refactor this code so that list is more narrowly typed, only allowing "matching" foo and bar properties, and then pass the forEach method a generic callback where foo and bar are annotated so that obj[foo] and bar are seen as identical types:

type KV = { [K in keyof T]-?: { foo: K, bar: NonNullable<T[K]> } }[keyof T]
/* type KV = {
    foo: "a";
    bar: string;
} | {
    foo: "b";
    bar: string;
} | {
    foo: "c";
    bar: number;
} */

const list: Array<KV> = [
    { foo: 'b', bar: 'str' },
    { foo: 'c', bar: 1 },
];

list.forEach(<K extends keyof T>(item: { foo: K, bar: NonNullable<T[K]> }) => {
    const { foo, bar } = item;
    obj[foo] = bar; // okay
});

The KV type does a little type juggling with mapped and lookup types to produce a union of all acceptable foo/bar pairs, which you can verify by using IntelliSense on the KV definition.

And the forEach() callback acts on a value of type item: { foo: K, bar: NonNullable<T[K]> } for generic K extends keyof T. So obj[foo] will be seen as type T[K], and you'll assign a NonNullable<T[K]> to it, which is acceptable according to a rule that isn't quite sound but convenient enough to be allowed.

Does that make sense? Hope that helps; good luck!

Link to code

发布评论

评论列表(0)

  1. 暂无评论