Nex Zhu
Nex Zhu

Reputation: 43

TypeScript: Why can't I assign a valid field of an object with type { a: "a", b: "b" }

I created the following types from a constant array <const>['a', 'b]:

const paths = <const>['a', 'b']

type Path = typeof paths[number]

type PathMap = {
  [path in Path]: path
}

Path equals to "a" | "b"

PathMap equals to {a: "a", b: "b"}

Then the following code compiles fine:

const BASE_PATHS = paths.reduce((map: PathMap, p: Path) => {
  map['a'] = 'a'
  return map
}, <PathMap>{})

This also works:

const BASE_PATHS = paths.reduce((map: PathMap, p: Path) => {
  return { ...map, [p]: p }
}, <PathMap>{})

But the following code does not compile:

const BASE_PATHS = paths.reduce((map: PathMap, p: Path) => {
  map[p] = p
  return map
}, <PathMap>{})

Which gave me this error at map[p] = p:

TS2322: Type 'string' is not assignable to type 'never'.   Type 'string' is not assignable to type 'never'.

Why is this the case?

Thanks for helping!

Upvotes: 4

Views: 562

Answers (2)

I believe this is because objects are contravariant in their key types.

For more information see this answer.

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

const paths = ['a', 'b'] as const

type Path = typeof paths[number]

type PathMap = {
    [path in Path]: path
}

type a = 'a'
type b = 'b'

type c = a & b // never

{
    const BASE_PATHS = paths.reduce((map: PathMap, p: Path) => {
        let x = map[p]
        map[p] = p // same here
        return map
    }, {} as PathMap)

Intersection of a and b produces never.

If you remove as const from paths it will compile, because string & string = string

Btw, since you are using functional approach try to avoid object mutations.

Here, in my blog, you can find more information about mutations in TS

Credits to @aleksxor

Here you can find official explanation

Upvotes: 4

Amir Saleem
Amir Saleem

Reputation: 3140

This is because map[p] will give you either a or b and type of a or b is definitely a string. For type Path, as it is a union type, type string is never for Path because Path must satisfy a or b. You can do something like this

const BASE_PATHS = paths.reduce((map: PathMap, p: Path) => {
    // enforce the compiler to treat map[p] as one of Path
    (map[p] as Path) = p;
    return map;
}, {} as PathMap);

Upvotes: 0

Related Questions