-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Asynchronous type guards and assertion signatures #37681
Comments
Partially a duplicate of #37515. Unfortunately neither of you respected the issue template. :-( |
Until this gets implemented, a fairly straightforward workaround is to return the input with the corresponding expected type if the check passes. For example, when checking some input against a Yup schema: export async function validateWithSchema<T, K>(data: T, schema: yup.Schema<K>) {
// `schema.validate` will throw on invalid input
await schema.validate(data);
return (data as unknown) as K;
} And then later on: const typedData = await validateWithSchema(unknownInput, schema); From there on, |
Is there any plan for this to be implemented? I think it would be incredibly useful especially as async functions are so widely used. The ops example is exactly how I would foresee it looking:
I understand you can get around it by personally asserting with "as", however I try to avoid doing this if at all possible. I find it can lead to future issues which would have been avoided if a proper assertion function was used instead, particularly in collaborated code P.S. thank you for all the hard work! I transitioned completely to TypeScript and haven't looked back since |
I don't believe the proposed workaround applies for a case like this: type Obj = { foo?: string };
async function defineFoo(obj: Obj): Promise<asserts obj is Required<Obj>> {
obj.foo = await getFooValue();
}
const obj: Obj = {};
await defineFoo(obj);
const foo: string = obj.foo; I'm currently dealing with a case like this, and the proposed feature would be quite nice to have! The synchronous version of my example already works (Playground Link). We're just unlucky that our case needs to be async. :-) |
Workaround Using Higher Order FunctionsTL;DR: We can't return DescriptionI found a workaround for asynchronous type guards/assertion functions that involves returning a synchronous assertion function or predicate function wrapped in a promise. It's not perfect, but I think it's as close as we can currently get to the ideal implementation. The Goal (not supported in TypeScript yet)const isStringAsync =
async (value: unknown): Promise<value is string> =>
typeof value === "string" My Workaroundconst isStringAsync =
async (value: unknown): Promise<(v: unknown) => v is string> =>
(v): v is string => typeof value === "string"
const aaa = 1 as number | string | Date
isStringAsync(aaa).then(isString => {
if (isString(aaa)) {
aaa // <- aaa: string
} else {
aaa // <- aaa: number | Date
}
}) Real World Use CaseI understand that checking if a value is a You can only save users to the database if they are valid. For a user to be valid, it needs to meet two criteria:
Modelsinterface User {
email: string
password: string
}
type PasswordValidated<T> = T & {
readonly __passwordValidated__: unique symbol
}
type UniqueEmailValidated<T> = T & {
readonly __uniqueEmailValidated__: unique symbol
}
type ValidatedUser = PasswordValidated<User> & UniqueEmailValidated<User> Functionsimport { fromPredicateFunction, assertHasProperties, AssertionFunction } from '@lucaspaganini/ts'
const isString = (v: unknown): v is string => typeof v === 'string'
const assertIsString: AssertionFunction<string> = fromPredicateFunction(isString)
const saveUserToDatabase =
async (validUser: ValidatedUser): Promise<void> => { ... }
const validateUserAsync = async (value: unknown): Promise<AssertionFunction<ValidatedUser>> => {
// If we throw an error, save it to throw later, in the assertion function
let errorToThrow: Error | null = null
try {
assertHasProperties(['email', 'password'], value)
assertIsString(value.email)
assertIsString(value.password)
// 1. The password should have at least 8 characters
if (value.password.length < 8)
throw Error('Password is too short')
// 2. The email cannot already belong to another user
if (await emailIsAlreadyTaken(value.email))
throw Error('Email is already taken')
} catch (error) {
errorToThrow = error
}
return v => {
if (errorToThrow)
throw errorToThrow
}
} Usagelet user: unknown
const validateUser: AssertionFunction<ValidatedUser> = await validateUserAsync(user)
validateUser(user)
await saveUserToDatabase(user) TypeScript Utilities LibraryI also made a TypeScript utilities library that includes (among other flexible utilities) a higher order function called import { makeAsyncPredicateFunction } from '@lucaspaganini/ts'
const isStringAsync =
makeAsyncPredicateFunction<string>(
async value => typeof value === 'string')
const aaa = 1 as number | string | Date
isStringAsync(aaa).then(isString => {
if (isString(aaa)) {
aaa // <- aaa: string
} else {
aaa // <- aaa: number | Date
}
}) It's MIT, open-sourced, and available on NPM. |
This would be very useful for Temporal signals and queries. let forced: string | undefined;
// either of:
await wf.condition /* <forced is string> */ (() => !!forced);
await wf.condition(() /* : forced is string */ => !!forced);
// forced expected type: string
// forced actual type: string | undefined |
I just ran into an issue that would greatly benefit from this in an API project. I want to be able to validate that an ID is of a certain type. All IDs are UUID v4 so I can't tell if an ID is of a user, or of some other entity, without doing a database lookup. The IDs are nominal so you can't pass a User ID or a string to a function that expects a Foobar ID so being able to narrow types with the built-in guards would be great as they don't leave too much space for error. So right now I'm torn between:
So in the end, I'm juggling between option no. 2 and 3, and I don't like either of them. Or I can use a flavor type for IDs, but that has it's own issues in my case. Hope this helps the team to make a decision. Cheers |
This proposed syntax is a bit confusing: async function assertIsCustomType(value: unknown): Promise<asserts value is CustomType> {
// ...
} Where is the return-type? The return-type is A return-type declaration such as How about fully separating the return-types from the assertion instead? async function assertIsCustomType(value: unknown): Promise<boolean>, asserts value is CustomType {
// ...
} I think this refactors better. It also allows you to return any type you want: async function assertIsCustomType(value: unknown): Promise<Stuff>, asserts value is CustomType {
// ...
} The default behavior of returning async function assertIsCustomType(value: unknown): void, asserts value is CustomType {
// ...
} This approach would allow making more than one assertion as well: async function assertThings(a: unknown, b: unknown): asserts a is A, asserts B is B {
// ...
} The way I see it, assertions functions currently don't have a defined return-type, because Why should the use of assertions prevent you from declaring a return-type? 🤔 |
Would be nice to have this. Currently, you can get a async function marked as a type guard to compile using async function loadRef<T>(
value: ManagedReference<T> | null,
// @ts-ignore
): asserts value is NonNullable<ManagedReference<T>> {}
// guards
loadRef(f.car)
console.log(f.car.color)
// does not guard
await loadRef(f.car)
console.log(f.car.color) |
I have need of this too, because the assertion I want to make has to be done with async browser APIs, namely I want to make a function that verifies that a particular object is a ‘fully signed transaction.’ It's not important what this means, it's only important to know that I have to operate over that object with an async method. // Pseudo-code, for the sake of brevity.
async function assertIsFullySignedTransaction(
tx: BaseTransaction & ITransactionWithSignatures,
): asserts tx is IFullySignedTransaction {
await Promise.all(tx.signers.map(async signer => {
const signatureDoesMatch = await crypto.subtle.verify(signer, /* ... */);
if (signatureDoesMatch === false) {
throw new Error(`Signature verification failed for ${signer}`);
}
}));
} |
- remove asyncInvariantFactory because async assertion functions are not supported. See microsoft/TypeScript#37681
- remove asyncInvariantFactory because async assertion functions are not supported. See microsoft/TypeScript#37681
Search Terms
async, asynchronous, promise, type guards, assertion signatures, asserts
Suggestion
TypeScript currently supports user-defined type guards and assertion signatures.
I think it would be great if we could also define custom asynchronous type guards and assertions.
Use Cases
This feature would allow to check types and validate data asnychonously. Please look at the examples below.
Examples
Imagine the code like this:
But sometimes, validation is done asynchronously, e.g. on server-side. Currently, you can achieve it this way:
But if TypeScript supported asynchronous assertions, you could skip
as Valid<User>
type assertion and let the TS do the job:Exactly the same issue could be presented for user-defined type guards.
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: