watwa.re

TypeScript satisfies your assertions

TypeScript 4.9 blessed us with the satisfies operator (see release notes). I recommend updating and migrating your as coercions over to satisfies since the latter makes sure that your type actually conforms.

Here is a little contrived example showing the difference:

type CorrectPassword = "%(&@#()%";
"123" as CorrectPassword; // OK
"123" satisfies CorrectPassword; // Type error

Another neat property of satisfies is that it retains the specific refined type, whereas simply overwrites it.

type EventType = string;

const e1 = "success" as EventType;
const e2 = "success" satisfies EventType;

if (
  e1 == "failure" || // OK, because e1 is of type string
  e2 == "failure" // Type error, e2 is still refined as "success"
) {
}

Recently I have been pining again for type-level assertions, specifically to get a poor person's exhaustive check (this is how the rich do it).

import assert from "assert";

function validateValue(
  fieldType: "string" | "number" | "boolean",
  value: string,
) {
  if (fieldType == "string") {
    assert(value.length > 0);
  } else if (fieldType == "number") {
    assert(!isNaN(Number(value)));
  } else {
    fieldType satisfies never; // Type error
    // fieldType has type "boolean" which does not satisfy never
  }
}

Now if we add the missing else-if branch it type-checks:

[...]
  } else if (fieldType == "boolean") {
    assert(typeof JSON.parse(value) == "boolean");
  } else {
    fieldType satisfies never;
  }
[...]

One might be tempted to remove the satisfies-assert but don't. Just like we keep runtime assertions (for branches types can't yet reach) in production code to be notified about unexpected data, so we should keep type-level assertions to be notified about unexpected effects of code changes.