(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
Petis not assignable to typeExtractCollection<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 馃槩
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:
if (name === "car") it wouldn't be able to verify the return type. #27808 and #33014 are possible ways to deal with that.ExtractCollection<> is a conditional type."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 馃槵
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
Tinside the implementation offindById(). 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: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:
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 linecollections[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 aswitch/caseonnamewithout actually having to do it.