Typescript: Type narrowing on promises - javascript

I'm trying to figure out how to use typeguards on promises based on parameters.
function request({ logic }: { logic: boolean }) {
return new Promise((resolve, reject) => {
if (l)
resolve("something");
resolve(1);
});
}
request({ logic: true }).then(a => {
a.length
})
In this example, I'd like to make sure that the typeof 'a' == 'string'. I tried writing some typeguards in request but their results get lost. I don't know if this is just a limitation of typescript or I just need to do some smart type casting or what.
This is a toy example of what I am actually trying to do, which is to make an async call whose result varies slightly based on some parameters. And I am loath to make another function just to cover an altered return type

Typescript function overloading to the rescue:
function request(logic: true): Promise<string>;
function request(logic: false): Promise<number>;
function request(logic: boolean) {
return new Promise((resolve, reject) => {
if (logic)
resolve("something");
resolve(1);
});
}
request(true).then(a => {
console.log(a.length); //<-- knows that a is a string
});
request(false).then(a => {
console.log(a.length); //<-- error: prop 'length' does not exist on number
});
Typeguards are meant to be used in if statements.
EDIT
You would be surprised! Typescript supports overloading distinction based on fields too! Check the following code:
function request(opts: { logic: true }): Promise<string>;
function request(opts: { logic: false }): Promise<number>;
function request(opts: { logic: boolean }) {
return new Promise((resolve, reject) => {
if (opts.logic)
resolve("something");
resolve(1);
});
}
request({ logic: true }).then(a => {
console.log(a.length); //<-- knows that a is a string
});
request({ logic: false }).then(a => {
console.log(a.length); //<-- error: prop length cannot be found on type number
});
EDIT
With a little generic magic you can achieve the desired behavior. This way only the logic field matters from the caller's point of view. Downside is that you loose typecheck even for opts.logic inside the request functions implementation.
function request<T extends { logic: true }>(opts: T): Promise<string>;
function request<T extends { logic: false }>(opts: T): Promise<number>;
function request(opts: any) {
return new Promise((resolve, reject) => {
if (opts.logic)
resolve("something");
resolve(1);
console.log(opts.anything);
});
}
request({ logic: true, foo: 'bar' }).then(a => {
console.log(a.length); //<-- knows that a is a string
});
request({ logic: false, foo: 'baz' }).then(a => {
console.log(a.length); //<-- error: prop length cannot be found on type number
});

Correct overloading is the next (should add type):
function request(logic: boolean): Promise<string>;
function request(logic: boolean): Promise<number>;
function request(logic: boolean): Promise<any>;
function request(logic: boolean) {
return new Promise((resolve, reject) => {
if (logic)
resolve("something");
resolve(1);
});
}
request(true).then((a) => {
console.log(a.length); //<-- knows that a is a string
});
request(false).then((a) => {
console.log(a.length); //<-- error: prop 'length' does not exist on number
});

Related

How to return a specific value type in a promise using Angular/Typescript

I'm stuck on something that I think should be pretty simple. We have a child component that accepts an input as such:
#Input() accessories: { [key: string]: Accessory };
And when the component is initialzed the template is able to read and display the values but we also need them in the component but they're not available. So we're trying to use setTimeout to force a change:
setTimeout(() => {
var resultArray = this.accessories;
if (this.accessories!= undefined) {
accessories.forEach(x =>
Object.keys(this.accessories).findIndex(key => {
if (this.accessories[key].accessoryId == x.accessoryId && this.accessories [key].code == "RECORD") {
this.isRecord= true;
}
}))}}, 2000);
But it still fires after we need it. I'm trying to implement a promise, which I haven't used before, so looking for some guidance on how to get this variable when it's available. I'm trying to wait for the result before continuing with the process.
You can construct a Promise like this:
function foo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const returnValue = 'foo';
resolve(returnValue);
}, 2000);
});
}
function main() {
foo().then(res => console.log(res)); // 'foo'
}

typescript, what is the type of a new Promise object?

In TS, I have a function like:
function a() : FOO {
return new Promise((resolve, reject) => {
//omitted
})
}
What is the proper type to write at FOO?
Is it good to just say Promise<any> ?
Perhaps it's helpful to show more details for what I am returning in function a:
new Promise((resolve, reject) => {
const cmd = spawn(str, null, { shell: true });
cmd.stdout.on("data", (data) => {
//just logging
});
cmd.stderr.on("data", (data) => {
return reject(data);
});
cmd.on("error", (error) => {
return reject(error);
});
cmd.on("close", (code) => {
return resolve(code);
});
});
It depends entirely on what type the fulfillment value of the promise will be. For instance, if the promise will be fulfilled with a number, then the type would be Promise<number>. For example:
function a() : Promise<number> {
return new Promise((resolve, reject) => {
doSomeCallbackBasedThing((err: Error | null, result: number) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
})
}
If it is fulfilled with undefined via resolve() with no argument, typically you'd write Promise<void>.
Often, you don't have to write the type explicitly at all; TypeScript can often infer it from the code in the function.
Re your edit: You'd use Promise<x> where x is the type of the code parameter you receive in your handler for the close event.

Types in object destructing from async function return in Typescript

Given a function that returns an object promise with a boolean & string:
const test = () => {
return new Promise(async (resolve) => {
resolve({ result: false, error: 'This is an error' });
})
}
I try to destruct these couple of values into constants:
const { result, error } = await test();
However, I always get these Typescript errors:
Property 'result' does not exist on type 'unknown'.ts(2339)
Property 'error' does not exist on type 'unknown'.ts(2339)
I've tried all kinds of combinations, but the only one working is adding the 'any' type, which I believe we should always avoid.
Any idea to define the right types without getting error?
Here you should add the type as the generic parameter to the promise - i.e new Promise<MyType>(...) Example:
type MyPromiseResult = { result: boolean, error: string };
const test = () => {
return new Promise<MyPromiseResult>((resolve) => {
resolve({ result: false, error: 'This is an error' });
})
}
async function doThing() {
const { result, error } = await test();
type ResultType = typeof result; // boolean
type ErrorType = typeof error; // string
}
Give your response a type
return new Promise<{ result: boolean, error: string }>((resolve) => {})

Angular using promise with generics inside promise

I need to interlace promises in my app:
protected scroll<T>(path: string, pageSize: number, filter: string, data: T[]): Promise<T[]> {
let promise = new Promise<T[]>(function(resolve, reject) {
this.httpClient
.get<T[]>(this.appConfigService.buildApiUrl(path), { params })
.toPromise<T[]>()
.then(result => {
if (result) {
resolve(data.concat(result));
}
})
.catch(function(e) {
reject(e);
});
});
return promise;
}
My problem is that I receive following message:
'Untyped function calls may not accept type arguments'
How would I solve this?
UPDATE:
I should not have removed the if condition from the example:
if (!filter) {
const params = new HttpParams()
.set('searchText', '')
.set('skip', data.length.toString())
.set('take', pageSize.toString());
const promise = new Promise<T[]>((resolve, reject) => {
this.httpClient
.get<T>(this.appConfigService.buildApiUrl(path), { params })
.toPromise<any>()
.then(result => {
if (result) {
resolve(data.concat(result));
}
resolve(data);
})
.catch(e => reject(e));
});
return promise;
}
// also returning a promise
return this.filter<T>(data, pageSize, filter, path);
There are a couple of problems there.
The error message is because you're using <T[]> on get and toPromise, which aren't generic functions. Just apply the type T to result in the then handler.
You're falling into the promise creation antipattern. You already have a promise (from this.httpClient), so you don't need new Promise.
You're using a traditional function for your new Promise callback, but then using this within it as though it were still referring to your class instance. If you were going to keep the new Promise, you'd want an arrow function instead, so it closes over this.
Instead (see *** comments):
protected scroll<T>(path: string, pageSize: number, filter: string, data: T[]): Promise<T[]> {
// *** Return the result of calling `then` on the promise from `toPromise`
return this.httpClient
// *** Don't use <T[]> on `get` and `toPromise`
.get(this.appConfigService.buildApiUrl(path), { params })
.toPromise()
.then((result: T) => { // *** <== Note the `T`
// *** If it's really possible that `result` will be falsy and you don't want that
// to be valid, you can do this:
if (!result) {
throw new Error("appropriate error here");
}
return data.concat(result);
});
}
On the playground
UPDATE:
I should not have removed the if condition from the example:
It doesn't matter, just put the body of the above into the if block:
protected scroll<T>(path: string, pageSize: number, filter: string, data: T[]): Promise<T[]> {
if (!filter) {
const params = new HttpParams()
.set('searchText', '')
.set('skip', data.length.toString())
.set('take', pageSize.toString());
return this.httpClient
// *** Don't use <T[]> on `get` and `toPromise`
.get(this.appConfigService.buildApiUrl(path), { params })
.toPromise()
.then((result: T) => { // *** <== Note the `T`
// *** If it's really possible that `result` will be falsy and you don't want that
// to be valid, you can do this:
if (!result) {
throw new Error("appropriate error here");
}
return data.concat(result);
});
}
return this.filter<T>(data, pageSize, filter, path);
}

Issue getting errorCallback from IPromise

I'm working with an existing TypeScript method and I'm struggling to get the errorCallback value from the promise. The Interface looks like the following from the Type Definition file for Angular:
interface IPromise<T> {
then<TResult>(successCallback: (promiseValue: T) => IHttpPromise<TResult>, errorCallback?: (reason: any) => any, notifyCallback?: (state: any) => any): IPromise<TResult>;
then<TResult>(successCallback: (promiseValue: T) => IPromise<TResult>, errorCallback?: (reason: any) => any, notifyCallback?: (state: any) => any): IPromise<TResult>;
then<TResult>(successCallback: (promiseValue: T) => TResult, errorCallback?: (reason: any) => TResult, notifyCallback?: (state: any) => any): IPromise<TResult>;
The TypeScript method I'm working with calls a service and the promise uses the return (this works):
public loadSavedLogin(): ng.IPromise<MyApp.Models.User> {
return this._myAppService.getUser(this.savedUserId).then((result: MyApp.Models.User) => {
if (result) {
this.userId = result.UserID;
this.userName = result.UserName;
}
return result;
});
}
The problem is I have no idea how to get the errorCallback value. If I place a comma after .then((result: MyApp.Models.User), I see Intellisense showing me the errorCallback parameter, but I just can't get any of the syntax working. In raw JS, I'd have a comma at the end with another function accepting the error value, but I'm not sure with this interface how to get the error returned.
How do I modify the function to get the error value if the service call returns one using IPromise?
Here is a simplified example to help you out.
class Test {
public _test: ng.IPromise<string>;
// This method has a return type of ng.IPromise<string>
// You must return a value of this type.
public example(): ng.IPromise<string> {
return this._test.then(
// Success
// Must return a string to be compatible with
// the ng.IPromise<string> return type
(val) => {
alert('Success');
return val;
},
// Error
// Should also return a string to be
// compatible with the return type
(reason) => {
alert('Error: ' + reason);
return '';
});
}
}
Because the example method return type is ng.IPromise<string>, the success function and the error function in the then method must return a string in order for the types to all match up.
In your case, they should return an instance of an MyApp.Models.User.
I suspect in your error function you weren't returning a value - but this makes the best common type between the success and error function void.
Further example... using just an array to show best common types when using functions:
var example = [
(input: string) => { return 'String'; },
(input: string) => { console.log(input); }
];
The best common type used in this example is (input: string) => void. Seems strange - but it actually makes sense. If you call the functions in this array, don't expect to get a return value.
So just make sure your success and error functions have the same return type and all the types will match up for you.
public loadSavedLogin(): ng.IPromise<MyApp.Models.User> {
return this._myAppService.getUser(this.savedUserId).then(
(result: MyApp.Models.User) => {
if (result) {
this.userId = result.UserID;
this.userName = result.UserName;
}
return result;
},
(reason: string) => {
return <MyApp.Models.User> null;
}
);
}

Categories