Reputation: 5287
I'm having an issue making a type detecting function that takes a partial amount of fields from a discriminated union and returns the matching type from the union. I have a create()
function that takes fields excluding timestamps and an ID and I return those fields with an ID and timestamps. We have the type
field in the argument and the return object but it's not being used to determine the return type.
interface Node {
type: string
id: string
createdAt: number
updatedAt: number
}
interface Pokemon extends Node {
type: 'Pokemon'
name: string;
}
interface Trainer extends Node {
type: 'Trainer'
name: string;
pokemon: string[]
}
type CreateNode<T extends Node> = T extends unknown
? Omit<T, 'id' | 'createdAt' | 'updatedAt'>
: never
function create<R extends Node>(obj: CreateNode<R>): R {
return {
...obj,
id: 'ds',
createdAt: 4,
updatedAt: 5,
} // give as error "... as is assignable to the constraint of type 'R', but 'R' could be instantiated with a different subtype of constraint 'Node'."
}
const pokemon = create<Pokemon|Trainer>({
type: 'Pokemon',
name: 'Garalaxapon'
})
pokemon //should be Pokemon but is Pokemon | Trainer
I don't understand the error in the return but I'm sure that's the crux of it. Thank you!
Upvotes: 1
Views: 1666
Reputation: 329513
I'm going to ignore the issue inside the implementation, which is a separate problem from the one you're asking about. For now I will assume that the implementation works, and use a declare
statement to focus on just the call signature issue.
The problem you're facing is that you are expecting too much from the single R
generic type. You are specifying it manually to let the compiler know which particular union of Node
subtypes you want to discriminate. But then you also want the compiler to somehow narrow R
to one of the members of that union. This doesn't work. Once you manually specify R
as a union, it's that union forever.
You could potentially solve this with two generic parameters; one (R
) you specify for the discriminated union, and one (T
) the compiler infers from the passed-in parameter. Unfortunately you can't do this in a single function signature; either you need to manually specify both R
and T
, or the compiler will try to infer both R
and T
. There's no partial type parameter inference (microsoft/TypeScript#26242) in TypeScript.
Sometimes in situations like this I use type parameter currying to split the single function of two type parameters into multiple one-type-parameter functions. In your case it would look like this:
declare function create<R extends Node>(): <T extends CreateNode<R>>(obj: T) => Extract<R, T>;
Note how R
corresponds to the full discriminated union, while T
refers to the type of the passed-in obj
parameter. You discriminate R
with T
by using the Extract
utility type: return only the elements of R
which are assignable to T
:
const createPokemonOrTrainer = create<Pokemon | Trainer>();
const pokemon = createPokemonOrTrainer({
type: 'Pokemon',
name: 'Garalaxapon'
}); // Pokemon
Here, create()
returns another function, and create<Pokemon | Trainer>()
returns the function you were trying to make before: something that accepts a partial-ish Pokemon | Trainer
and discriminates it to just Pokemon
or Trainer
.
But maybe you don't actually even need R
at all here; are you planning to call create()
with a different discriminated union each time? If you're only ever going to use a single union type like Pokemon | Trainer
, then you can just hardcode it. Essentially, don't bother with the over-generic create()
and just manually write createPokemonOrTrainer()
:
type DiscrimUnion = Pokemon | Trainer;
declare function createDiscrimUnion<T extends CreateNode<DiscrimUnion>>(obj: T): Extract<DiscrimUnion, T>;
const pokemonAlso = createDiscrimUnion({
type: 'Pokemon',
name: 'Garalaxapon'
}); // Pokemon
Okay, hope that helps; good luck!
Upvotes: 1
Reputation: 276105
pokemon //should be Pokemon but is Pokemon | Trainer
This is correct. Reason:
You have const pokemon = create<Pokemon|Trainer>
where create<R extends Node>(obj: CreateNode<R>): R
. Since create
returns the R
that is passed in, pokemon will be the R
that you are passing in. You are passing in Pokemon|Trainer
So pokemon: Pokemon|Trainer
Use Pokemon
if that is what you want i.e. create<Pokemon>
Upvotes: 0