typescriptjavascript-objectstypescript-genericscontravariance

Contravariant object value in Typescript


Consider the following overloaded Typescript function (attempt):

function eraseField(o : Record<"field", string>, nullify : false) : void
function eraseField(o : Record<"field", string | null>, nullify : true) : void
function eraseField(o : any, nullify : any) : void
{
    if (nullify)
    {
        o.field = null;
    }
    else
    {
        o.field = "";
    }
}

Now, consider the following object:

let o : Record<"field", string> = {"field" : "toBeErased"};

Unfortunately, we can do the following without any errors:

eraseField(o, true); // 'o.field' is no longer a string, which is against the type declaration.

How can I tweak the function declarations/definitions so that the above line of code gives a Typescript error (in strict mode)? In particular, I want to prevent the widening of Record<"field", string> to Record<"field", string | null> when it is passed to "eraseField", thus effectively making the Record type contravariant for its values in that context.


Solution

  • You can use a constrained generic type parameter in your relevant overload signature, which will allow you to use the compiler's inference to conditionally determine whether the parameter's field value allows for a nullable value. In the case that it does, you can accept it — and in the case that it doesn't you can deny its use by setting the parameter's type to never:

    TS Playground

    function eraseField(o: Record<"field", string>, nullify: false): void;
    function eraseField<T extends Record<"field", string | null>>(
      o: T extends Record<"field", string> ? never : T,
      nullify: true,
    ): void;
    function eraseField(o: Record<"field", string | null>, nullify: boolean) {
      if (nullify) o.field = null;
      else o.field = "";
    }