Typescript: Inferred generic type gets lost when trying to return value

Created on 11 Feb 2020  路  2Comments  路  Source: microsoft/TypeScript

(sorry for the bad title, I really didn't know how to describe this at all, and I've tried reaching out to some other developers who hade no idea either 馃槵 For the same reason I've also had a hard time searching for similar issues, and didn't really find anything)

TypeScript Version: 3.8.0-dev.20200208 (and 3.7.5)

Search Terms: "is not assignable to"

Code

interface Base { _id: string }
interface Car extends Base { make: string }
interface Pet extends Base { name: string }

interface Collection<T> {
    find(filter: Partial<T>): Promise<T[]>
}

declare const collections: {
    car: Collection<Car>
    pet: Collection<Pet>
}

type ExtractCollection<T extends keyof typeof collections> = (typeof collections)[T] extends Collection<infer R> ? R : never



// $ExpectType Promise<Pet[]>
let a = collections.pet.find({ name: 'test' })

// $ExpectType Car
let b: ExtractCollection<'car'>

// $ExpectType Car | Pet
let c: ExtractCollection<'car' | 'pet'>

// $ExpectType Promise<Car[]>
let d = findById('car', 'test')



function findById<T extends keyof typeof collections>(name: T, id: string): Promise<Array<ExtractCollection<T>>> {
    return collections[name].find({ _id: id }) // <------ Type 'Pet' is not assignable to type 'ExtractCollection<T>'
}

Expected behavior:

I expected no error since ExtractCollection<'pet'> resolves to Pet.

Actual behavior:

Type Pet is not assignable to type ExtractCollection<T>

Playground Link: http://www.typescriptlang.org/play/?ts=3.8.0-dev.20200208&ssl=1&ssc=1&pln=33&pc=1#code/JYOwLgpgTgZghgYwgAgEJwM4oN7IPrAAmAXMhmFKAObIC+AUKJLIigMJxTIQAekIhDGkw5kAWzgBrCKXKUQNBk2jwkyAAoQw3PhAFD0WZLhBwxMshWp16jcCtbI2AewA2riAjDBnIADwAKgB8xvTI4cgwoIQAFFGuzKTqnN5wroFBAJRJUM5iwFiBANoAukH0DPSEnq6cKAi+5MgN7p7ejaTYYREInKQurV4+-hxQ5RHIAA5a-W4eQ75+mmDllWAAntPIAKJ8UIhgA-Pt-gE6-ILI0uvOMMgb07fNc23DGCEAvMgxDxBPLcc3pkigESuc9Jcjq9FqAYNBkAAlEIAfkRyFIIAgADdoLZbAB6fHIAAku2mXgCmxQ6ly+UKy1K5Q82jgyC+AOhIAwADppmBuVEBDETGYLABySDkMV0TIEomknjksCUrajejM5AAI1IuwoByhC38Yt6UDF5XohJJZLaKvYnGQAB8NFp1Vpmjq9vqXoa-MbONKnWK+Wa5VbFTaqRpaQUIH5RozXdpCGzItFUOsAJKxP2mgA0yAlEClsvoMAAriBDamBOms4FwforhAbndfv9vSd3jFTOZSAF80RZFYFNko3kY34AIJQfbrPy6-ZeA0nDJBEJdCZQLRlqAgZ6DTtFHsQEoC6LC-CD5BEGUVehAA

Related Issues: no 馃槩

Design Limitation

Most helpful comment

The compiler's not really adept at verifying that values are assignable to types that depend on as-yet-unspecified generic type parameters, such as the T inside the implementation of findById(). You're going to want to use a type assertion or similar type-safety-loosening technique to get that to compile with no error, as in:

function findById<T extends keyof typeof collections>(name: T, id: string) {
    return collections[name].find({ _id: id }) as Promise<Array<ExtractCollection<T>>>;
}

The closest I can imagine to getting the compiler to verify the types is to represent what you're doing as an indexing operation, like this:

function findById<T extends keyof typeof collections>(name: T, id: string) {
    return {
        get car() { return collections.car.find({ _id: id }) },
        get pet() { return collections.pet.find({ _id: id }) }
    }[name]
}

Related issues, although I'm not sure which one if any this duplicates:

13995 the compiler doesn't narrow type parameters so even if you tested if (name === "car") it wouldn't be able to verify the return type. #27808 and #33014 are possible ways to deal with that.

33912 the compiler really doesn't know how to verify assignability to conditional types depending on unspecified generics, and ExtractCollection<> is a conditional type.

30581 or #25051, even if both of the above were implemented, I think you'd still need to write redundant code so that the compiler would narrow both to "car" and "pet" separately and verify that each case returns the proper type. Having a single line collections[name].find({ _id: id }) would still end up being some kind of non-generic union type. It would be nice to ask the compiler to pretend that you wrote a switch/case on name without actually having to do it.

All 2 comments

The compiler's not really adept at verifying that values are assignable to types that depend on as-yet-unspecified generic type parameters, such as the T inside the implementation of findById(). You're going to want to use a type assertion or similar type-safety-loosening technique to get that to compile with no error, as in:

function findById<T extends keyof typeof collections>(name: T, id: string) {
    return collections[name].find({ _id: id }) as Promise<Array<ExtractCollection<T>>>;
}

The closest I can imagine to getting the compiler to verify the types is to represent what you're doing as an indexing operation, like this:

function findById<T extends keyof typeof collections>(name: T, id: string) {
    return {
        get car() { return collections.car.find({ _id: id }) },
        get pet() { return collections.pet.find({ _id: id }) }
    }[name]
}

Related issues, although I'm not sure which one if any this duplicates:

13995 the compiler doesn't narrow type parameters so even if you tested if (name === "car") it wouldn't be able to verify the return type. #27808 and #33014 are possible ways to deal with that.

33912 the compiler really doesn't know how to verify assignability to conditional types depending on unspecified generics, and ExtractCollection<> is a conditional type.

30581 or #25051, even if both of the above were implemented, I think you'd still need to write redundant code so that the compiler would narrow both to "car" and "pet" separately and verify that each case returns the proper type. Having a single line collections[name].find({ _id: id }) would still end up being some kind of non-generic union type. It would be nice to ask the compiler to pretend that you wrote a switch/case on name without actually having to do it.

Thanks for the great writeup @jcalz! 鉂わ笍

I've been going thru the linked issues to read and subscribe, lots of great info there. As you say, I'm not sure that any of those would actually fix the problem, but they should absolutely be steps in the right direction!

For now I've worked around this in my app with a simple @ts-ignore 馃槵

Was this page helpful?
0 / 5 - 0 ratings

Related issues

OliverJAsh picture OliverJAsh  路  242Comments

yortus picture yortus  路  157Comments

jonathandturner picture jonathandturner  路  147Comments

blakeembrey picture blakeembrey  路  171Comments

tenry92 picture tenry92  路  146Comments