I am trying to create a function like this:
function assert<T>(condition: T, msg?: string): T & asserts condition {
if (!condition) {
throw new Error(msg)
}
return condition
}
The goal is to return condition
unmodified, preserving its type, but also informing the compiler that the returned value is truthy. This would allow it to be used as a standalone statement, but also unobtrusively inside an expression:
const success: bool = true
// ... some code that might set success = false
assert(success)
const element: HTMLElement = assert(document.getElementById('foo'))
Unfortunately, the above syntax does not work (playground). Is there a way to express this in TypeScript?
I can't find any official documentation on assertion functions except the Release notes for TypeScript 3.7, and those only demonstrate assertion functions that implicitly return void
.
The best I can come up with is a workaround for object types only, because those can never be falsy:
function assert<T extends object>(condition: T | null | undefined, msg?: string): T {
if (!condition) {
throw new Error(msg)
}
return condition
}
CodePudding user response:
You can use NonNullable<T>
:
function assert<T>(condition: T, msg?: string): NonNullable<T> {
return condition ?? error(msg);
}
// workaround for https://github.com/microsoft/TypeScript/issues/18535
function error(msg?: string): never {
throw new Error(msg);
}
const s: string | null = null;
const n: string = assert(s);
CodePudding user response:
There is a fixed, finite set of falsy values in Javascript. Some of them are not writable as literal types in Typescript, but the rest are 0
, ''
, false
, null
and undefined
. You can use Exclude
to remove them from the type parameter T
:
type Truthy<T> = Exclude<T, 0 | '' | false | null | undefined>
function checkTruthy<T>(x: T): Truthy<T> {
// ...
}
Or as an assertion function:
function assertTruthy<T>(x: T): asserts x is Truthy<T> {
// ...
}
Unfortunately, as the error message in your original code says, you can't write a function type annotation that does both of these. That said, there should be no need to; if the assertion function would just return its argument unchanged, you can always refactor a call to it so that it's not in an expression:
// change this
let y = assertTruthy(x) 'foo';
// to this
assertTruthy(x);
let y = x 'foo';
Note that Typescript isn't able to represent types like "a non-empty string" or "a non-zero number", so e.g. Truthy<string>
is just the same as string
and you get no benefit regardless of whether you return or assert. So using Exclude
on strings or numbers only really helps if you have a union of literals like 'foo' | 'bar' | ''
.