Search code examples
typescripttypesnullnullabletypescript-types

Why does typescript disregard strict null checks when implementing abstract functions?


Consider the following abstract class:

export abstract class Foo {
  abstract bar(param: string | null): string
}

Changing the parameter to be non-nullable in a concrete implementation does not cause a type error. This is unexpected.

export class ConcreteFoo extends Foo {
  bar(param: string): string {
    return param
  }
}

This allows me to do:

const inst = new ConcreteFoo() as Foo
const res = inst.bar(null)
console.log(res) // res is null, but typescript says it is string

Why is this possible?

Strict null checks are on. Typescript version is 4.5.3.


Solution

  • Method parameter types are checked bivariantly, so as long as the parameters types of the implementation and the abstract signature are related in either direction it will type check (in this case string is a sub type of string | null)

    You could use function signature syntax to avoid this but only for interfaces (derived classes can't implement function members as methods unfortunately ex):

    export interface Foo {
      bar: (param: string | null) => string
    }
    
    export class ConcreteFoo implements Foo{
      bar(param: string /* | null */): string { // error
        return param!
      }
    }
    

    Playground Link

    There is currently no flag to change this behavior (as of TS 4.6). You can read a little bit about the reasoning behind this decision in the PR that introduced strictFunctionTypes

    If you want to learn more about what variance is and how it works in typescript you can check out my talk on the topic