typescript

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


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?


Solution

  • 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.