Why must a type predicate's type be assignable to its parameter's type?
Asked Answered
F

3

29

I have a type predicate:

// tslint:disable-next-line:no-any
const isString = (value: any): value is string {
  return typeof value === 'string'
}

This works, but it requires that I disable my linter. I would rather do this:

const isString = <T>(value: T): value is string {
  return typeof value === 'string'
}

That way the type is not any but instead we have 1 typeguard function for each type, that is one function a -> Boolean for every a.

Typescript complains that:

A type predicate's type must be assignable to its parameter's type. Type 'string' is not assignable to type 'T'.

This doesn't make sense to me... Why should it matter what the type predicate's type is?

Fortyfive answered 19/5, 2018 at 1:35 Comment(4)
You could use (value: {}): value is string => { ... } instead. As for the error, it's weird; it seems backwards. T needing to be assignable to string would make more sense, to me. E.g TypeScript should know that Date will never be assignable to string, so such a predicate would make no sense.Kuska
Hmm... but allowing us to use (value: any) => value is string as a type predicate is worse, right? I can pass it a Date if I want.Fortyfive
It's a little different. Using T is more like writing a whole bunch of functions. Try writing a user-defined type guard like (value: Date): value is string => { ... } and you'll see what I mean.Kuska
Good point, the generic means "give me one of each function a -> Boolean" so if any of those functions doesn't exist, then the generic doesn't exist. Nice! Could you write up an answer?Fortyfive
K
30

A user-defined type guard performs a runtime check to determine whether or not value of a particular type satisfies a type predicate.

If there is no relationship between the value's type and the type in the type predicate, the guard would make no sense. For example, TypeScript won't allow a user-defined guard like this:

function isString(value: Date): value is string {
    return typeof value === "string";
}

and will effect this error:

[ts] A type predicate's type must be assignable to its parameter's type.
Type 'string' is not assignable to type 'Date'.

A Date value will never be a string, so the guard is pointless: its runtime check is unnecessary and should always return false.

When you specify a generic, user-defined type guard, T could be anything, so - as with Date - for some types, the type guard would make no sense.

If you really don't want to use any, you could use an empty interface - {} - instead:

function isString(value: {}): value is string {
    return typeof value === "string";
}

If you also want to allow for null and undefined values to be passed to the guard, you could use:

function isString(value: {} | null | undefined): value is string {
    return typeof value === "string";
}

Regarding the error message, the predicate type must be assignable to the value type because the type guard is used to check whether a value with a less-specific type is in fact a value with a more-specific type. For example, consider this guard:

function isApe(value: Animal): value is Ape {
    return /* ... */
}

Ape is assignable to Animal, but not vice versa.

Kuska answered 19/5, 2018 at 9:34 Comment(3)
Then why doesn't it work when I add the extends keyword to the generic type, and literally put the same type as the output predicate. const isString = <T extends string>(value: T): value is string { return typeof value === 'string' }Melmon
T extends string is never going to be wider than string, so what is that type guard even supposed to do? Type guards narrow an argument's type.Kuska
Precisely. The predicate is not even a subtype in my example, yet it refuses to work. The point is that even without narrowing the type, the predicate refuses to work. If you did narrow, it definitely doesn't work: const isString = <T extends string>(value: T): value is 'sdf' => { return value === 'sdf' }. I know this is a contrived answer, and you could just move the type directly into the parameter, without using a generic. But the issue persists in a real world problem. The present comment is merely the smallest reproducible example.Melmon
V
10

Adding to the accepted answer, if you happen to need to use a type guard against a mixin, you'll get this error too, since the is operator doesn't behave as an implements would.

interface Animal { ... }

interface Climber { ... }

interface Ape extends Animal, Climber { ... } 

const isClimberMixin = (animal: Animal): animal is Climber => ...

Such code fails because Climber is not assignable to Animal since it doesn't extend from it.

The solution, if you can't avoid the mixin pattern, is to use an union type:

const isClimberMixin = (animal: Animal): animal is Animal & Climber => ...
Virility answered 21/6, 2020 at 7:38 Comment(0)
K
5

Typescript 3.0 introduces the unknown type that works very well for this case, and I believe should be used for any typeguard function that accept any type as parameter.

const isString = (value: unknown): value is string => {
  return typeof value === 'string'
}

This is also helpful inside the function body, as it will require you to check the type of the value (with a typeof or instanceof) before using methods and properties of a specific type. This is relevant for complex typeguard functions.

More on unknown: https://mariusschulz.com/blog/the-unknown-type-in-typescript

Kerakerala answered 30/5, 2022 at 9:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.