Reputation: 2554
So Here is a summary of my observation working with typescript.
Here is some code:
type someTypeEnum = '1';
type someOtherTypeEnum = '2' | '3';
type combinedTypeEnum = someTypeEnum | someOtherTypeEnum;
Here is the first case :-
function typeAssertion<T extends combinedTypeEnum>(args: T): args is someTypeEnum {
// The error i get
// A type predicate's type must be assignable to its parameter's type.
// Type '"1"' is not assignable to type 'T'.
return undefined;
}
I cannot understand why this thing fails coz we have already limited our arguments to combinedTypeEnum, in case we do
typeAssertion('4')
We already get an error stating '4'
is not a valid argument so why is it that args is someTypeEnum
is considered an in-valid predicate.
Here is the second case :-
function typeAssertion(args: combinedTypeEnum): args is someTypeEnum {
return undefined;
}
This seems to work fine but in case we do this :-
function someFunction<T extends combinedTypeEnum>(args: T): T {
if (typeAssertion(args)) {
// args here is 'T & "1"'
args
}
return args
};
why is it that we have T & "1" and not only "1", we specifically asserted that it is someTypeEnum.
I was really curious as to why such decisions were made. It would be really helpful to see how things break in case things were done in a different manner.
Upvotes: 4
Views: 5069
Reputation: 9980
UPDATE:
Or much simpler:
function typeAssertion(args: combinedTypeEnum): args is someTypeEnum {
return args === "1";
}
See this playground
Original:
This had me stumped for a long while as well. Actually the solution (at least in 2021, not sure if it was back when the question was asked) is:
function typeAssertion<T extends combinedTypeEnum>(args: T): args is T & someTypeEnum {
return args === "1";
}
The idea behind this (as far as I understand it from this answer) is this: when you call typeAssertion("2")
, the T
gets the value "2"
(literal type "2"
), meaning that you end up with the function:
function typeAssertion(args: "2"): args is someTypeEnum
which obviously doesn't make sense. I'm not sure the workaround (with T &
) makes more sense, but it works:
type someTypeEnum = '1';
type someOtherTypeEnum = '2' | '3';
type combinedTypeEnum = someTypeEnum | someOtherTypeEnum;
function typeAssertion<T extends combinedTypeEnum>(args: T): args is T & someTypeEnum {
return args === "1";
}
const a: combinedTypeEnum = "1"
const b: combinedTypeEnum = "2"
const c: combinedTypeEnum = "3"
const d = "1"
const e = "2"
const f = "4"
let one: "1" = "1"
if (typeAssertion(a)) one = a
if (typeAssertion(b)) one = b
if (typeAssertion(c)) one = c
if (typeAssertion(d)) one = d
if (typeAssertion(e)) one = e
if (typeAssertion(f)) one = f // this one gives an error
See in Playground
Upvotes: 1
Reputation: 26828
extends
doesn't make much sense when you have string literals. To make the explanation easier let me use other types. Consider these three classes:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
when we use generics the actual type is set by the caller:
function foo<T extends Animal>(arg: T) {}
foo(new Dog()); //T is Dog, equivalent to foo(arg: Dog) {}
foo(new Cat()); //T is Cat, equivalent to foo(arg: Cat) {}
Now you may already see where we are going. Let's use a type predicate:
function foo<T extends Animal>(arg: T): arg is Cat {}
When we call foo(new Dog())
the last example becomes equivalent to this:
function foo(arg: Dog): arg is Cat {}
And of course it doesn't work or make sense.
As for your second example: The type of the variable doesn't change. The point is that by asserting a specific type the compiler allows you to do whatever can be done with this type.
Upvotes: 1