Reputation: 3312
I'm trying to make a function that has 2 parameters, where the type of the 2nd parameter is based on the type of the 1st. I can see several threads that this question might be a duplicate of but the typing syntax can get so complex that I've been fiddling with this for a while now and can't get it right.
class Mammal { }
class Bird { }
function stuff(classType: typeof Mammal | typeof Bird, arg: number or string depends on first param) {
if (classType extends Mammal) {
console.log(arg * 5);
} else {
console.log(arg.split(" "))
}
}
stuff(Mammal, 5); // want this to be valid
stuff(Bird, "Hello world"); // want this to be valid
stuff(Bird, 5); // want this to be invalid
Upvotes: 0
Views: 326
Reputation: 20162
class Mammal {
static type = 'mammal' as const
}
class Bird {
static type = 'bird' as const
}
function stuff<
M extends typeof Mammal | typeof Bird,
A extends (M extends (typeof Mammal) ? number : string)>
(classType: M, arg: A) {
if (classType.type === 'mammal') {
console.log((arg as number) * 5);
} else {
console.log((arg as string).split(" "))
}
}
stuff(Mammal, 5); // ok
stuff(Mammal, 'a'); // error
stuff(Bird, "Hello world"); // ok
stuff(Bird, 1); // error
TS is structural typed language, that means that if you have two classes which have the same definition, they are just the same for TS, as there is no structural difference between them. Below proove of this statement:
class Mammal {}
class Bird {}
type MammalIsBird
= Mammal extends Bird
? Bird extends Mammal
? true
: false
: false // evaluates to true, Mammal and Bird are equal
That is the reason why in order to distinguish both Mammal
and Bird
we need to create some static property to have the difference.
Another thing is that when we ask about this property in for example Mammal
there will be none, as Mammal
has all non-static properties, we need to use typeof Mammal
in order to have the interface with static ones. That is why in the implementation I am using typeof M
and not M
.
Few important informations:
M extends typeof Mammal | typeof Bird
means we allow or class Mammal or class Bird, if I would say just Mammal | Bird
it would mean I want to get instances of the class not a class itselfA extends (M extends (typeof Mammal) ? number : string)
- conditional type, if we get Mammal
type as M
we have second argument number
if not it is string
classType.type === 'mammal'
thanks of having static property we can use it in condition(arg as number)
type assertion needs to be done, as discriminant for first property doesn't work for the second one.You can avoid type assertion by working with one object/array type of arguments. But I don't recommend this approach, nevertheless here you go:
type MammalArgs = [typeof Mammal, number]
type BirdsArgs = [typeof Bird, string];
const isMammalClass = (x: MammalArgs | BirdsArgs): x is MammalArgs => x[0].type === 'mammal'
const isBirdArgs = (x: MammalArgs | BirdsArgs): x is BirdsArgs => x[0].type === 'bird'
function stuff<
M extends typeof Mammal | typeof Bird,
A extends (M extends (typeof Mammal) ? MammalArgs : BirdsArgs)>
(...args: A) {
if (isMammalClass(args)) {
console.log(args[1] * 5); // no type assertion
}
if (isBirdArgs(args)) {
console.log(args[1].split(" ")); // no type assertion
}
}
What I have done above is joining our arguments into one type [typeof Mammal, number] | [typeof Bird, string]
in order to achieve relation between discriminant in first argument with the second. Having both in one type allows on such relation. The issue is that I needed to create two type guards, and also needed to use array directly, as any destructuring would broke our type narrowing. I would choose the first approach with type assertion.
Upvotes: 1