Skip to content

andjsrk/typesafe-guard

Repository files navigation

typesafe-guard

ℹ️ The package is ESM-only.

A utility for creating type-safe user-defined type guard for TypeScript.

Motivation

Writing predicate functions is not type-safe because TS trusts the implementation completely even it is invalid:

const isObject = (x: unknown): x is object => true // always true!
const strOrObj: string | object = 'some string'
if (isObject(strOrObj)) {
	// now TS treats `strOrObj` as an object, even actually it is not
}

So I made a more safe way to writing predicate functions —not only for predicate functions, actually— by requiring narrowing the value to the goal type!

Usage

First, write a validator:

interface User {
	name: string
	age: number
}
const User = validatorFor<User>()(function*(x) {
	// we do not `throw` errors, we `yield` errors
	// so delegate an error from `props` if present, otherwise take the value with narrowed type
	const user = yield* props({
		name: string,
		age: number,
	})(x)
	
	// you can make the validation fail by `throw yield ...`
	// note that we still need `throw` while yielding errors, to block resuming the generator
	if (user.age <= 0) throw yield 'The user\'s age is not positive.'
	
	return user
})

// examples of INVALID usage
const User = validatorFor<User>()(function*(x) {
	const user = yield* object(x)
	
	return user // error, because the value is not narrowed enough
})
const User = validatorFor<User>()(function*(x) {
	const user = yield* object(x)
	
	// error, because the function does not return anything
})

Then, use the validator wherever you want:

// for validation
const res = validate(someValue, User) // Result<User, string>
if (res.ok) {
	// validation succeeded
	const user = res.value // user: User
} else {
	// validation failed
	const reason = res.reason // reason: string
}

// for predicate function
if (is(someValue, User)) {
	// someValue: User
}

// for assertion
assertIs(someValue, User)
// someValue: User

ℹ️ An alternative way to const foo = yield* someValidator(x), you can use the assertion function require():

validatorFor<User>()(function*(x) {
	require(x, yield* props({
		name: string,
		age: number,
	})(x))
	// now x is narrowed
	
	return x
})

ℹ️ You can write a validator first, then derive a type from it:

const User = validator(function*(x) {
	const user = yield* props({
		name: string,
		age: number,
	})(x)
	
	return user
})
type User = ValidationTargetOf<typeof User>
// type User = {
//   name: string
//   age: number
// }

ℹ️ If your validator is just a combination of other validators, you can just eject the contents of the validator:

const User = props({
	name: string,
	age: number,
})
// or if you want to check its type
const User = validatorFor<User>()(props({
	name: string,
	age: number,
}))

is(someValue, User) // ok

ℹ️ All validators are just a validator, whether yours or built-ins:

is(someValue, string) // ok

validator(function*(x) {
	const user = yield* User(x) // ok
	
	return user
})