Search code examples
typescriptimmutabilityreadonlymapped-typestype-assertion

Can I fix this typescript compiler error without forcing the type by assertion?


At this typescript playground I have written a minimal Immutable (recursive readonly) type and a Store interface which stores an Immutable.

Currently there is a compile error for the read() and write() functions of my partitioned store class, which I would like to resolve by properly annotating the types, and not making type assertions. Here are the top-level definitions.

interface Store<State>{
  read:() => Immutable<State> 
  write:(state:Immutable<State>) => Immutable<State>
}

export type ImmutableObject<T> = 
  {
    readonly [K in keyof T]: Immutable<T[K]>;
  }
;

export type Immutable<T> = T extends object
  ? ImmutableObject<T>
  : T extends string | number | boolean | null
  ? Readonly<T>
  : never;

My problem arises when I derive a partitioned sub-store from a store. That means just selecting a sub-part of the State using one of its declared keys, and making a Store out of that.

This is my draft implementation which requires the commented assertion to suppress compile errors...

class BasicStorePartition<SuperState, Key extends keyof SuperState> 
  implements Store<SuperState[Key]>{
    constructor(
    readonly store: Store<SuperState>,
    readonly key: Key,
  ) {}
  //this line requires the commented type assertion in order to compile
  read = () => this.store.read()[this.key] // as unknown as Immutable<SuperState[Key]>
  write = (state:Immutable<SuperState[Key]>) => { 
    this.store.write({
      ...this.store.read(),
      [this.key]:state
    });
    return this.read()
  }
}

The compile errors reported are as below. I would expect these to resolve to the same type and no idea where the string as a key has come from, since no key is annotated that way in the source...

Type 'ImmutableObject<SuperState[string]>' is not assignable to type 'Immutable<SuperState[Key]>'

Type 'Immutable<SuperState>[Key]' is not assignable to type 'Immutable<SuperState[Key]>'

It is possible to fix the compilation by simply asserting the type information like...

as unknown as Immutable<SuperState[Key]>

I want to resolve this compile error so the types resolve and align to the expected readonly types, without me having to bypass the compiler. Can anyone see a way to do this?

Welcome any other observations about improving the approach.


Solution

  • I'm afraid that there is no way for the compiler to know beyond any doubt that the types you want it to infer are correct, even though in practice they probably will be (there are a lot of edge cases in TypeScript).

    Here are my observations on your problem:

    • The object type is deprecated, see this question. Apparently people are having problems with its usage.

    • In my opinion it is okay to use type assertions in your implementation if you are 99% sure it is correct. The compiler is not perfect and never will be. But never ever give back wrong types from your public interface, or force the user of your functions to do assertions. I encounter quite a lot of libraries with poor or wrong public type information, and that can become really frustrating.

    I am authoring the Rimbu immutable collections library, in which I have taken the greatest care to make the type information as correct and precise as possible. However, if you look 'under the hood' so to speak, the code is full of type assertions because the compiler, especially in more advanced cases, just needs help. This is true in any typed language, but a bit more often in TypeScript because it has so many features. I am fine with this as long as I can offer my users a much better experience.

    As a side note, your type definition comes pretty close to Rimbu's Immutable definition