'Class implements type with generic method but does not conform to the type

I wish I had a more minimal example of the issue here. This is pretty short, but I can't seem to reproduce this issue without using type logic over tuples.

I'm defining types for a key-value store where keys can be tuples.

type Key = (string | number)[]
type KeyValuePairs = [Key, any]

Here's an example schema.

type Schema = 
    | [["a", number], number]
    | [["b", string], string]

I want to be able to define the database interface separate from the class so that there can be different backends / implementation.

type KeyValueStoreApi<S extends KeyValuePairs> = {
    get: <T extends S[0]>(key: T) => Extract<S, {0: T}>[1]
}

That return type appears to be what is causing me trouble as we'll see later. Anyways, I have an implementation of this type with a class:

class KeyValueStore<S extends KeyValuePairs> implements KeyValueStoreApi<S> {
    get<T extends S[0]>(key: T): Extract<S, {0: T}>[1] {
        return {} as any
    }
}

It's the EXACT same type interface...

Thus functions in my program references the type rather than the class so that the implementations are interchangeable.

function f(db: KeyValueStoreApi<Schema>) {}

But typescript is unhappy.

const db = new KeyValueStore<Schema>()
f(db) // TypeError

Here's the error:

Argument of type 'KeyValueStore<Schema>' is not assignable to parameter of type 'KeyValueStoreApi<Schema>'.   The types returned by 'get(...)' are incompatible between these types.
    Type 'Extract<[["a", number], number], { 0: T; }>[1] | Extract<[["b", string], string], { 0: T; }>[1]' is not assignable to type 'Extract<[["a", number], number], { 0: T; }>[1] & Extract<[["b", string], string], { 0: T; }>[1]'.
      Type 'Extract<[["a", number], number], { 0: T; }>[1]' is not assignable to type 'Extract<[["a", number], number], { 0: T; }>[1] & Extract<[["b", string], string], { 0: T; }>[1]'.
        Type 'number' is not assignable to type 'Extract<[["a", number], number], { 0: T; }>[1] & Extract<[["b", string], string], { 0: T; }>[1]'.
          Type 'number' is not assignable to type 'Extract<[["b", string], string], { 0: T; }>[1]'.
            Type 'number' is not assignable to type 'string'.
              Type 'Extract<[["a", number], number], { 0: T; }>[1]' is not assignable to type 'Extract<[["b", string], string], { 0: T; }>[1]'.
                Type 'Extract<[["a", number], number], { 0: T; }>' is not assignable to type 'Extract<[["b", string], string], { 0: T; }>'.
                  Type '{ 0: T; } & [["a", number], number]' is not assignable to type 'Extract<[["b", string], string], { 0: T; }>'.(2345)

In fact, the class doesn't conform at all even though it's implementing the type!

const store: KeyValueStoreApi<Schema> = new KeyValueStore<Schema>() // same error

Any ideas what's going wrong here and how to fix it?

Playground link


Edit 1

I've been able to reproduce this issue using objects instead of tuples: playgound. But it hasn't been too insightful really.

The main thing I've noticed is that if I get rid of the [1] part: Extract<S, {0: T}>[1]Extract<S, {0: T}>, then I don't have any type errors... Pretty annoying though, because I need that to properly specify the type...



Solution 1:[1]

I figured it out! Though I could use help coming up with the language to describe the problem... I stumbled upon this solution with guess-and-check. I found this article and tried something similar using

type DistributiveProp<T, K extends keyof T> = T extends unknown
    ? T[K]
    : never;

Then I replaced Extract<S, {0: T}>[1] with DistributiveProp<Extract<S, {0: T}>, 1>. No idea why it worked honestly... Thoughts?

Playground link

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Chet