Compare:
function validateNonEmpty<T>(list: T[]): void {
if (list[0] === undefined)
throw Error("list cannot be empty")
}
function parseNonEmpty<T>(list: T[]): [T, ...T[]] {
if (list[0] !== undefined) {
return list as [T, ...T[]]
} else {
throw Error("list cannot be empty")
}
}
function assertNonEmpty<T>(list: T[]): asserts list is [T, ...T[]] {
if (list[0] === undefined) throw Error("list cannot be empty")
}
function checkEmptiness<T>(list: T[]): list is [T, ...T[]] {
return list[0] !== undefined
}
declare const arr: number[]
// Error: Object is possibly undefined
console.log(arr[0].toLocaleString())
const parsed = parseNonEmpty(arr)
// No error
console.log(parsed[0].toLocaleString())
if (checkEmptiness(arr)) {
// No error
console.log(arr[0].toLocaleString())
}
assertNonEmpty(arr)
// No error
console.log(arr[0].toLocaleString())
For me the `${arg} is ${type}` approach is superior as you are writing the validation once and can pass the precise mechanism for handling of the error to the caller, who tends to have a better idea of what to do in degenerate cases (sometimes throwing a full on Exception is appropriate, but sometimes a different form of recovery is better).