Repository navigation
User-defined type assertions #17760
Description
Proposal: support user-defined type assertions (keyword as) in addition to user-defined type guards (keyword is).
A user-defined type assertion is a function with a return type expressed as in the following example (continuing the handbook example in "Advanced Types").
function assertIsFish(pet: Fish | Bird): pet as Fish {
if (!isFish(pet))
throw new Error("birds are not supported");
}
assertIsFish(pet);
pet.swim();:pet as fish means that, if the function assertIsFish returns without throwing, pet is automatically asserted to be of type Fish.
Use-cases:
-
Validators: user-defined type assertions make it possible to encapsulate in functions the type narrowing that is currently available through control flow analysis. They also allow types that better describe the behavior of many existing libraries, including: node asserts, data validators that throw detailed errors, many test frameworks.
-
Polyfills: enables describing functions that modify objects by adding new properties.
addName<T>(obj: T): obj as T & { name: string } {
obj.name = "Mike";
}
const obj = {};
addName(obj);
obj.name.indexOf("i");Validator examples:
simple undefined checking
function notUndefined<T>(obj: T | undefined): obj as T {
if (obj === undefined)
throw new Error("invalid object");
}
const myObj: string | undefined = {} as any;
// [ts] Object is possibly 'undefined'.
myObj.indexOf("");
notUndefined(myObj);
// no errors
myObj.indexOf("");joi-like validation currently requires implementing the schema description of an object for runtime checking and copying the schema to an interface to have static type checking while the static type could be inferred by the implementation of the schema
interface Schema<T> {
validate(obj: any): obj is T;
}
function attemptValidation<T>(obj: any, schema: Schema<T>): obj as T {
if (!schema.validate(obj))
throw new Error("very detailed object validation error");
}
// complex schema implements T and T is inferred by the schema construction
// this is already possible
const complexSchema = Joi.object({
myField: Joi.date().iso()
});
attemptValidation(objFromTheRealWorld, complexSchema);
const day = objFromTheReadWorld.myField.getDay();
Activity
jcalz commentedon Aug 14, 2017
I like the idea. I've needed something like this before, and the way I've done it is by immediately throwing if the relevant type guard returns
false. So you can have the somewhat cumbersome:It would be nice to have something less ugly. Not sure if the
x as Tsyntax is the way to go? Maybe we can continue to usex is Tbut the control flow analysis can tell that the returned value is alwaystrueand therefore any call to the type guard does automatic narrowing? So something liketrue & (x is T), if it compiled, would be a possibility.maghis commentedon Aug 14, 2017
Joe Calzaretta (@jcalz) I agree, I'm open to any syntax, that was just a suggestion as a starting point.
Another really annoying thing is in tests, where you first assert that an object is truthy and then you have to explicitly check for it with an
ifor a type assertion before you can assert on values of the object.mhegazy commentedon Aug 22, 2017
also related to #10421
TazmanianD commentedon Oct 12, 2017
I was just looking for something like this as well. I agree with Joe Calzaretta (@jcalz) that it seems like the existing syntax should continue to work and it's just the compiler code analysis can take into account that the failure in the function means the code throws and exits and a non-failure confirms the assertion.
maghis commentedon Oct 19, 2017
TazmanianD I agree, we don't necessarily need new syntax, it was just nice to know by just looking at the function signature that I don't have to check for a return value.
mitchellwills commentedon Oct 22, 2017
I was just looking for something like this as well. I don't think you can use the same
x is Treturn value syntax because you won't always be able to do control flow analysis on the called function, such as if it's referencing a function declaration.I'm also not sure that the original return value
x as Tsyntax works either since it doesn't really have any thing to do with the return value of the function, just that the function actually returned. It's more of a postcondition. Having a syntax that goes in the return value slot would prohibit having an actual return value from the function. For example:Unfortunately, I don't really have any better suggestions.
MicahZoltu commentedon May 27, 2018
There are a number of ES built-in functions that would greatly benefit from this like
Number.isFinite(). Currently the following will not work:Instead, you must do something like:
maghis commentedon Jun 5, 2018
Micah Zoltu (@MicahZoltu) In that case it's because
paramis already defined asnumberin the type definitions.You could achieve the same results with the existing syntax if the function was defined as
(param: any) => param is number.MicahZoltu commentedon Jun 5, 2018
Adding
param is numberwas my hope, but apparently it doesn't actually work in this case as described here: #24436 (comment)maghis commentedon Jun 6, 2018
Micah Zoltu (@MicahZoltu) that makes sense, I didn't think about the side effects.
aciccarello commentedon Jan 28, 2019
I'm also running into a similar situation to Massimo Andreasi Bassi (@maghis) in tests as #17760 (comment). I'd like for
assert.isDefined(param)to be able to be typed the same as an if that throws an error.RyanCavanaugh commentedon Jan 28, 2019
Anthony Ciccarello (@aciccarello) FYI that behavior is tracked at #10421 / #8655
ikokostya commentedon Jan 29, 2019
Ryan Cavanaugh (@RyanCavanaugh) Issue #8655 was closed without good explanation. Many questions in the thread are still without answers. Suggested
throw expressionsdoesn't solve all cases, e.g.process.exit()in Node.js.jwalton commentedon Mar 29, 2019
Testing with chai is another obvious place where this would be nice:
I feel like there should be some sort of "condtional type" syntax that could be used here. Something like:
mheiber commentedon Jun 4, 2019
In order to meet our use case, user-defined type assertions would have to be intersectable with normal return values. In the below, I use this syntax instead of
as T:Ensures<T>.For example, suppose that you have a UI framework where children only have
.layoutproperty if they have been added to a parent.It would be really helpful if we could plug in to the same sort of flow analysis used by type guards.
Example of desired behavior using
Ensures:ethanresnick commentedon Feb 24, 2020
This should probably be closed now that #32695 has been merged (although that PR still doesn't cover the request in the comment immediately above, to be able to return a normal value and assert with the same function).
maghis commentedon Mar 27, 2020
I think so,
assertscovers pretty much all the listed use cases.Anyone against me closing?
DanielRosenwasser commentedon Apr 28, 2022
Looks like this was fixed in #32695