The followings are what I wanna achieve:
function isString<T>( value: T ): value is T extends string ? T : string {
return typeof value === "string";
}
function isNotString<T>( value: T ): T extends string ? unknown : T {
return typeof value !== "string";
}
But I receive an error that says, "A type predicate's type must be assignable to its parameter's type." which is expected.
I don't wanna achieve the followings btw:
function isString<T>( value: T ): Extract<T, string> {
return typeof value === "string";
}
function isNotString<T>( value: T ): Exclude<T, string> {
return typeof value !== "string";
}
Because, both of these may infer the never
type that I don't want. That says, I always need to have a known type to deal with.
Can you please help me achieve this TypeScript way? :-)
Thank you so much for your insightful answers, guys. Here're my replies:
Don't bother creating an isNotString type guard because it won't do what you want. Instead, check that isString is false and typescript will infer everything properly.
Do you see anything wrong with the following function? If I'm not wrong, I think it achieves what I expect to.
function isNotString<T>( value: T ): value is Exclude<T, string> {
return typeof value === "string";
}
I personally like to include a generic and assert that value is T & string rather than just value is string so that no information is lost if the type is already known to be a specific subset of string.
I like this approach, but I achieved this previously in a weird way. I'm not going to share this here, as it was an insane approach.
We didn't have any never in our return statement, but we still get never in our code because it's the only logical type if our a: string variable is not string.
Can't agree more. I was surely out of my mind yesterday. :-)
It's worth noting that typescript's built-in type guards don't actually refine the type in the negative case. Which just goes to show that you can't really do it properly when dealing with an unknown type like T.
I do agree dear. I just wanna have it for verbose purpose. Maybe I'm wrong, but isNotString
seems more verbose to me than ! isString
.
Can you please take a look at this Playground?
I can see Exclude
is working as expected, but value: T | string
is not. Feel free to point out if I missed something.
Don't bother creating an isNotString
type guard because it won't do what you want. Instead, check that isString
is false and typescript will infer everything properly.
@Andrei's answer is correct that a type guard must assert its return with value is
. I want to address the issue of never
. A type guard is a true
/false
check. One of those branches checks out and the other is illogical so it will be never
.
Let's say that this is our type guard. I personally like to include a generic and assert that value is T & string
rather than just value is string
so that no information is lost if the type is already known to be a specific subset of string
.
function isString<T>( value: T ) : value is T & string {
return typeof value === "string";
}
Now we want to use this type guard in an if
statement. Let's see what happens when we do that with a variable whose type is already known.
const a: string = "something";
if (isString(a)) {
console.log(a); // `a` here is `string`
} else {
console.log(a); // `a` here is `never`
}
We didn't have any never
in our return statement, but we still get never
in our code because it's the only logical type if our a: string
variable is not string
.
Defining an isNotString
typeguard isn't really necessary because you could just as well check ! isString()
. But if we want to do it, it is going to return never
when called with a string. Anything else wouldn't make sense. The assertion is applied only when the function is true. So if a string
is not a string
, then what is it?
Honestly it is really hard to define a type guard that asserts a negative case, ie. value is not string
, because type guards are designed to assert a positive. Exclude
seems like it should work, but it's not able to discriminate unions because the union doesn't extend string
so it always returns never
. I can see why this is undesirable behavior that you don't want. So just don't do this. You already have a guard that sees if something is a string
, so use that to see if it's not a string
and it will do what you want.
function checkUnion(value: string | number) {
if (!isString(value)) {
console.log(value); // `value` is `number`
}
if (isString(value)) {
console.log(value) // `value` is `string`
}
}
It's worth noting that typescript's built-in type guards don't actually refine the type in the negative case. Which just goes to show that you can't really do it properly when dealing with an unknown type like T
.
function checkGeneric<T>(value: T) {
if ( typeof value !== "string" ) {
console.log(value); // value is still just T
}
if ( typeof value === "string" ) {
console.log(value); // value is T & string
}
}