0

After a while I'm working again on TypeScript, but I'm stuck at this issue.

export class CrossBrowserStorage<T> {
  getValue<P extends keyof T>(
    key: P,
    defaultValue: T[P]
  ): Observable<T[P]> {
    return this.getValues({ [key]: defaultValue }).pipe(map(values => values[key]));
                      ----^ TS2345: Argument of type '{ [x: string]: T[P]; }' is not assignable 
                            to parameter of type 'Partial '
  }

  getValues(keys: Partial<T>): Observable<Partial<T>> {
    return from(browser.storage.sync.get(keys) as Promise<Partial<T>>);
  }
}

TS2345: Argument of type '{ [x: string]: T[P]; }' is not assignable to parameter of type 'Partial'

Can't understand how to solve this while maintaining correct typings.

1
  • This is a duplicate of a mixture of this question and this question: the computed key gets widened to string; and even if you narrow it to P, the compiler still can't verify in the generic case. You'll need to use a type assertion like { [key]: defaultValue } as Pick<T, P> as Partial<T>. If those other answers don't cover your issue let me know Commented Feb 28, 2020 at 19:04

1 Answer 1

1

Unfortunately the compiler won't be able to verify this for you, and in cases like this where you know more the compiler, a reasonable solution is to use a type assertion:

class Class<T> {
    f<K extends keyof T>(k: K, v: T[K]) {
        const badPartial: Partial<T> = { [k]: v }; // error!
        const goodPartial: Partial<T> = { [k]: v } as Pick<T, K> & Partial<T>; // okay
    }
}

So that's what you need to do. But why is it like this? There seem to be two main stumbling blocks that the compiler is facing.


The first stumbling block is that the compiler only knows how to interpret computed properties if the key is a single statically known literal or unique symbol type:

const k1 = "a";
const o1 = { [k1]: 123 }; 
// const o1: {[k1]: number};
o1.a; // okay
o1.b; // error

If, instead, if the key type is generic or a union of string literals, the compiler widens it to string and treats the resulting object as one with a string index signature, to strange results:

const k2 = Math.random() < 0.5 ? "a" : "z";
const o2 = { [k2]: 123 };
// const o2: {[x: string]:number};
o2.a; // no error, but might not exist
o2.b; // no error, but *definitely* doesn't exist

function foo<K extends string>(k3: K) {
    const o3 = { [k3]: 123 };
    // const o3: {[x: string]:number};
    o3.a; // no error, but probably doesn't exist
}

This is an open issue in TypeScript; see microsoft/TypeScript#13948. It's sort of clear what the "right thing" to do would be for the case of unions of literals; presumably o2 above should be of type {a: number} | {z: number}. It's less clear with generics; perhaps o3 above should be of type Partial<Record<K, number>>. In any case, a string index signature isn't great and breaks things like you've seen.


Another stumbling block is that even if the compiler realized that the computed property was assignable to some suitable type depending on generic types T and K, it probably wouldn't be able to recognize that this type is assignable to Partial<T>. It's easy enough for a human being to see this, but the compiler can't necessarily reason about higher order type manipulation of this sort. The canonical open issue about this is probably microsoft/TypeScript#28884, where the issue is that Pick<T, K> & Omit<T, K> is not seen as compatible with T, when T or K is generic. For any statically known type T and K, the compiler could do the analysis: Pick<{a: string, b: string}, "a"> & Omit<{a: string, b: string}, "a"> is indeed seen as assignable to {a: string, b: string}. But once you make the types generic, the compiler essentially gives up.


For both of those reasons, there's little chance of getting the compiler to verify the assignability for you. And the best advice I can give is to use that type assertion and move on.

Okay, hope that helps; good luck!

Playground link to code

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

1 Comment

Thank you very much! This is a super complete answer. I don't particularly like adding code when it shouldn't be needed, but hey, nevermind this time.

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.