4

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!

1
  • I believe second option is the best approach to stick with Commented Jun 30, 2021 at 7:26

2 Answers 2

4

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

Sign up to request clarification or add additional context in comments.

6 Comments

In complement to your answer. Here is microsoft/TypeScript/pull/30769 the exact PR introduced this behaviour for indexed access types when union type converts into intersection when met on the target side of assignment.
@aleksxor thanks a lot for the link! I made an update of answer
Just checked your blog. Very good resources are available. Thanks for sharing
I am also in the process of advancing TS skills. I want to write a blog, a book on Typesript.
Don't forget to share the link to your blog
|
0

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);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.