├── README.md ├── generate-promise.ts └── Promise.lua /README.md: -------------------------------------------------------------------------------- 1 | # Typed Roblox Promises 2 | 3 | This is a wrapper that adds full Luau type support for 4 | [roblox-lua-promise](https://github.com/evaera/roblox-lua-promise). It does this 5 | using some fancy Luau type trickery. The result is a way more pleasant 6 | experience when using Promises in luau where everything is fully typed, even 7 | when you chain promises. 8 | 9 | ## Installation 10 | 11 | To install, simply copy the Promise.lua script into your project and modify the 12 | `game.ReplicatedStorage.Packages.UntypedPromise` at the bottom with the path to 13 | roblox-lua-promise. 14 | 15 | ## Usage 16 | 17 | You can simply use this typed wrapper the same way you'd use 18 | `roblox-lua-promise` itself. This is a drop-in replacement that you will 19 | immediately reap the benefits of in your existing code. 20 | 21 | ## What's so special about this? 22 | 23 | This wrapper fully supports chaining promises, which most other wrappers are 24 | unable to. It does this by repeating the type definition of Promise multiple 25 | times. This is because recursive types are not supported in Luau, making this 26 | neccessary to have a correct definition for promises. 27 | ![image](https://github.com/fewkz/typed-luau-promise/assets/83943819/8fec9389-1ca3-407b-ae0e-b2dc19278fdd) 28 | 29 | ## Why is there a generation script? 30 | 31 | Instead of having to manually write out a repeated type definition 10 times, 32 | there's a `generate-promise.ts` script that does this for us, making it very 33 | easy to modify the definition without having to make the change multiple times. 34 | To run the generation script, simply do 35 | `deno run generate-promise.ts > Promise.lua` 36 | 37 | ## Things you may run into 38 | 39 | ### Incomplete definition 40 | 41 | This wrapper doesn't define every single function in roblox-lua-promise because 42 | I never got around to it. Adding new ones is relatively straight forward, and 43 | may be added whenever I run into the need. Feel free to contribute a pull 44 | request adding any missing definitions. 45 | 46 | ### Promise.all 47 | 48 | The definition of `Promise.all` requires all of the promises passed in to have 49 | the same non-variable return type. This is so that the output promise can be 50 | typed as `Promise<{ T }>`. It will cause issues if you have code like this: 51 | 52 | ```lua 53 | local pA = fetchA() -- returns `string` 54 | local pB = fetchB() -- returns `number` 55 | local pC = fetchC() -- returns `boolean` 56 | local a, b, c = unpack(Promise.all({pA, pB, pC}):expect()) -- errors since promise types aren't uniform 57 | ``` 58 | 59 | You should ideally rewrite the code to look this instead: 60 | 61 | ```lua 62 | local pA = fetchA() 63 | local pB = fetchB() 64 | local pC = fetchC() 65 | local a = pA:expect() 66 | local b = pB:expect() 67 | local c = pC:expect() 68 | ``` 69 | 70 | ### Generic variable type 71 | 72 | For cases where a variable may need to store a generic promise, you can use the 73 | `AnyPromise` type. This stores any promise, however you don't get any type info 74 | about what the promise will return. 75 | 76 | ```lua 77 | local p: Promise.AnyPromise 78 | 79 | p = Promise.resolve("hello") 80 | p = Promise.resolve(5) 81 | p = p:andThen(function(r) -- r is typed as `any` 82 | return r 83 | end) 84 | ``` 85 | -------------------------------------------------------------------------------- /generate-promise.ts: -------------------------------------------------------------------------------- 1 | // To run, deno run generate-promise.ts > Promise.lua 2 | // When editing, may be useful to install watchexec via scoop (scoop install watchexec) 3 | // and run: watchexec --watch generate-promise.ts 'deno run generate-promise.ts > Promise.lua' 4 | 5 | // deno-lint-ignore no-explicit-any 6 | function l(strings: TemplateStringsArray, ...values: any[]): void { 7 | console.log( 8 | strings 9 | .reduce((result, str, i) => { 10 | const value = values[i] ? values[i] : ""; 11 | return result + str + value; 12 | }, "") 13 | .replace(/^ +/gm, ""), // This line trims all beginning spaces, excluding tabs 14 | ); 15 | } 16 | 17 | // If a successHandler or failureHandler return a Promise, the Promise will be chained on. 18 | // Therefore, we type our handlers as a union of functions, the first 19 | // returning PromiseLike and the second returning T2... 20 | // This has the consequence of the function passed to andThen not registering it's 21 | // parameters correctly, instead being inferred from the body of the function. 22 | // For example, the following code: 23 | // local function foo(a: number) end 24 | // Promise.resolve("hi"):andThen(function(b) 25 | // foo(b) 26 | // end) 27 | // Will infer b as a number, rather than know that it's a string. 28 | // And it will precede to give a very verbose error about how: 29 | // Type '(number) -> ()' could not be converted into '((string) -> (a...)) | ((string) -> PromiseLike)'; 30 | // Therefore, we put it behind a flag that we have off, until Luau becomes a little smarter. 31 | // Until then, a replacement for :andThenCall(promise) is :andThen(function() return promise:expect() end) 32 | const allowReturningPromises = false; 33 | 34 | l`-- fewkz/typed-luau-promise 2023`; 35 | l`-- Generated by generate-promise.ts`; 36 | l`type PromiseStatus = "Started" | "Resolved" | "Rejected" | "Cancelled"`; 37 | l`type PromiseLike = { expect: (self: PromiseLike) -> T..., [any]: any }`; 38 | function promiseType(name: string, next: string) { 39 | const generic = name == "PromiseExhausted" || name == "_AnyPromise" 40 | ? "...any" 41 | : "T..."; 42 | const genericNext = next == "PromiseExhausted" || name == "_AnyPromise" 43 | ? next 44 | : next + ""; 45 | 46 | // Changing self to "any" fixes bug involving andThenCall 47 | // We change it to PromiseLike<{$generic}> to prevent people from 48 | // accidentally doing dot function calls instead of colon function calls 49 | const self = `PromiseLike<${generic}>`; 50 | l`type ${name} = {`; 51 | 52 | l`\tandThen: (`; 53 | l`\t\tself: ${self},`; 54 | if (allowReturningPromises) { 55 | l`\t\tsuccessHandler: (((${generic}) -> PromiseLike) | ((${generic}) -> T2...))?,`; 56 | l`\t\tfailureHandler: (((...any) -> PromiseLike) | ((...any) -> T2...))?`; 57 | } else { 58 | l`\t\tsuccessHandler: ((${generic}) -> T2...)?,`; 59 | l`\t\tfailureHandler: ((...any) -> T2...)?`; 60 | } 61 | l`\t) -> ${genericNext},`; 62 | 63 | l`\tandThenCall: (`; 64 | l`\t\tself: ${self},`; 65 | if (allowReturningPromises) { 66 | l`\t\tcallback: ((A...) -> PromiseLike) | ((A...) -> T2...),`; 67 | } else { 68 | l`\t\tcallback: (A...) -> T2...,`; 69 | } 70 | l`\t\tA...`; 71 | l`\t) -> ${genericNext},`; 72 | 73 | if (allowReturningPromises) { 74 | l`\tandThenReturn: (self: ${self}, PromiseLike) -> ${genericNext}`; 75 | l`\t\t| (self: ${self}, T2...) -> ${genericNext},`; 76 | } else { 77 | l`\tandThenReturn: (self: ${self}, T2...) -> ${genericNext},`; 78 | } 79 | l`\tcancel: (self: ${self}) -> (),`; 80 | 81 | l`\tcatch: (`; 82 | l`\t\tself: ${self},`; 83 | if (allowReturningPromises) { 84 | l`\t\tfailureHandler: ((...any) -> PromiseLike) | ((...any) -> T2...)`; 85 | } else { 86 | l`\t\tfailureHandler: (...any) -> T2...`; 87 | } 88 | l`\t) -> ${genericNext},`; 89 | 90 | l`\texpect: (self: ${self}) -> ${generic},`; 91 | l`\tfinally: (self: ${self}, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> ${genericNext},`; 92 | l`\tgetStatus: (self: ${self}) -> PromiseStatus,`; 93 | l`\tnow: (self: ${self}, rejectionValue: any?) -> ${name}`; 94 | l`}`; 95 | } 96 | 97 | l`-- stylua: ignore start`; 98 | promiseType("PromiseExhausted", "PromiseExhausted"); 99 | promiseType("Promise8", "PromiseExhausted"); 100 | promiseType("Promise7", "Promise8"); 101 | promiseType("Promise6", "Promise7"); 102 | promiseType("Promise5", "Promise6"); 103 | promiseType("Promise4", "Promise5"); 104 | promiseType("Promise3", "Promise4"); 105 | promiseType("Promise2", "Promise3"); 106 | promiseType("Promise1", "Promise2"); 107 | 108 | // For cases where you might want to have a variable that stores *any* promise. 109 | // The following code, usually, would error: 110 | // local promise = Promise.resolve() 111 | // promise = Promise.resolve():andThenReturn(3) 112 | // So, by typing `promise` as `Promise`, it will solve this issue. 113 | // However, it sacrifices the inability for 114 | promiseType("_AnyPromise", "any"); 115 | l`export type AnyPromise = _AnyPromise`; 116 | 117 | l`type PromiseLib = {`; 118 | l`\tStatus: { 119 | \t\tStarted: "Started", 120 | \t\tResolved: "Resolved", 121 | \t\tRejected: "Rejected", 122 | \t\tCancelled: "Cancelled", 123 | \t},`; 124 | l``; 125 | // For Promise.all, we make it so that all promises should return the same result. 126 | // If you want promises to return different results, you should instead do :andThenReturn() 127 | // for each promise in the list, to make them return nothing, and then, instead of reading 128 | // the table passed to the new promise, you should use :expect() on the promises directly. 129 | // This retains the type info. 130 | // For example: 131 | // local p1 = Promise.resolve("3 plus 1 is: "); local p2 = Promise.resolve(3) 132 | // Promise.all({p1:andThenReturn(nil), p2:andThenReturn(nil)}):andThen(function() 133 | // print(p1:expect() .. (p2:expect() + 1)) 134 | // end) 135 | // This does not support passing in promises that return (), aka nothing. 136 | // Therefore, all ()-returning promises must go through :andThenReturn(nil) 137 | l`\tall: (promises: { PromiseLike }) -> Promise1<{ T }>,`; 138 | l`\tdelay: (seconds: number) -> Promise1,`; 139 | l`\tfromEvent: (event: RBXScriptSignal, predicate: ((T...) -> boolean)?) -> Promise1,`; 140 | l`\tnew: (( 141 | \t\tresolve: (T...) -> (), 142 | \t\treject: (...any) -> (), 143 | \t\tcancel: ((callback: (() -> ())?) -> boolean) -> () 144 | \t) -> ()) -> Promise1,`; 145 | l`\tresolve: (T...) -> Promise1,`; 146 | l`\ttry: (callback: (A...) -> T..., A...) -> Promise1,`; 147 | l`}`; 148 | l`-- stylua: ignore end`; 149 | l``; 150 | l`return require(game:GetService("ReplicatedStorage").Packages.UntypedPromise) :: PromiseLib`; 151 | -------------------------------------------------------------------------------- /Promise.lua: -------------------------------------------------------------------------------- 1 | -- fewkz/typed-luau-promise 2023 2 | -- Generated by generate-promise.ts 3 | type PromiseStatus = "Started" | "Resolved" | "Rejected" | "Cancelled" 4 | type PromiseLike = { expect: (self: PromiseLike) -> T..., [any]: any } 5 | -- stylua: ignore start 6 | type PromiseExhausted = { 7 | andThen: ( 8 | self: PromiseLike<...any>, 9 | successHandler: ((...any) -> T2...)?, 10 | failureHandler: ((...any) -> T2...)? 11 | ) -> PromiseExhausted, 12 | andThenCall: ( 13 | self: PromiseLike<...any>, 14 | callback: (A...) -> T2..., 15 | A... 16 | ) -> PromiseExhausted, 17 | andThenReturn: (self: PromiseLike<...any>, T2...) -> PromiseExhausted, 18 | cancel: (self: PromiseLike<...any>) -> (), 19 | catch: ( 20 | self: PromiseLike<...any>, 21 | failureHandler: (...any) -> T2... 22 | ) -> PromiseExhausted, 23 | expect: (self: PromiseLike<...any>) -> ...any, 24 | finally: (self: PromiseLike<...any>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> PromiseExhausted, 25 | getStatus: (self: PromiseLike<...any>) -> PromiseStatus, 26 | now: (self: PromiseLike<...any>, rejectionValue: any?) -> PromiseExhausted 27 | } 28 | type Promise8 = { 29 | andThen: ( 30 | self: PromiseLike, 31 | successHandler: ((T...) -> T2...)?, 32 | failureHandler: ((...any) -> T2...)? 33 | ) -> PromiseExhausted, 34 | andThenCall: ( 35 | self: PromiseLike, 36 | callback: (A...) -> T2..., 37 | A... 38 | ) -> PromiseExhausted, 39 | andThenReturn: (self: PromiseLike, T2...) -> PromiseExhausted, 40 | cancel: (self: PromiseLike) -> (), 41 | catch: ( 42 | self: PromiseLike, 43 | failureHandler: (...any) -> T2... 44 | ) -> PromiseExhausted, 45 | expect: (self: PromiseLike) -> T..., 46 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> PromiseExhausted, 47 | getStatus: (self: PromiseLike) -> PromiseStatus, 48 | now: (self: PromiseLike, rejectionValue: any?) -> Promise8 49 | } 50 | type Promise7 = { 51 | andThen: ( 52 | self: PromiseLike, 53 | successHandler: ((T...) -> T2...)?, 54 | failureHandler: ((...any) -> T2...)? 55 | ) -> Promise8, 56 | andThenCall: ( 57 | self: PromiseLike, 58 | callback: (A...) -> T2..., 59 | A... 60 | ) -> Promise8, 61 | andThenReturn: (self: PromiseLike, T2...) -> Promise8, 62 | cancel: (self: PromiseLike) -> (), 63 | catch: ( 64 | self: PromiseLike, 65 | failureHandler: (...any) -> T2... 66 | ) -> Promise8, 67 | expect: (self: PromiseLike) -> T..., 68 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise8, 69 | getStatus: (self: PromiseLike) -> PromiseStatus, 70 | now: (self: PromiseLike, rejectionValue: any?) -> Promise7 71 | } 72 | type Promise6 = { 73 | andThen: ( 74 | self: PromiseLike, 75 | successHandler: ((T...) -> T2...)?, 76 | failureHandler: ((...any) -> T2...)? 77 | ) -> Promise7, 78 | andThenCall: ( 79 | self: PromiseLike, 80 | callback: (A...) -> T2..., 81 | A... 82 | ) -> Promise7, 83 | andThenReturn: (self: PromiseLike, T2...) -> Promise7, 84 | cancel: (self: PromiseLike) -> (), 85 | catch: ( 86 | self: PromiseLike, 87 | failureHandler: (...any) -> T2... 88 | ) -> Promise7, 89 | expect: (self: PromiseLike) -> T..., 90 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise7, 91 | getStatus: (self: PromiseLike) -> PromiseStatus, 92 | now: (self: PromiseLike, rejectionValue: any?) -> Promise6 93 | } 94 | type Promise5 = { 95 | andThen: ( 96 | self: PromiseLike, 97 | successHandler: ((T...) -> T2...)?, 98 | failureHandler: ((...any) -> T2...)? 99 | ) -> Promise6, 100 | andThenCall: ( 101 | self: PromiseLike, 102 | callback: (A...) -> T2..., 103 | A... 104 | ) -> Promise6, 105 | andThenReturn: (self: PromiseLike, T2...) -> Promise6, 106 | cancel: (self: PromiseLike) -> (), 107 | catch: ( 108 | self: PromiseLike, 109 | failureHandler: (...any) -> T2... 110 | ) -> Promise6, 111 | expect: (self: PromiseLike) -> T..., 112 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise6, 113 | getStatus: (self: PromiseLike) -> PromiseStatus, 114 | now: (self: PromiseLike, rejectionValue: any?) -> Promise5 115 | } 116 | type Promise4 = { 117 | andThen: ( 118 | self: PromiseLike, 119 | successHandler: ((T...) -> T2...)?, 120 | failureHandler: ((...any) -> T2...)? 121 | ) -> Promise5, 122 | andThenCall: ( 123 | self: PromiseLike, 124 | callback: (A...) -> T2..., 125 | A... 126 | ) -> Promise5, 127 | andThenReturn: (self: PromiseLike, T2...) -> Promise5, 128 | cancel: (self: PromiseLike) -> (), 129 | catch: ( 130 | self: PromiseLike, 131 | failureHandler: (...any) -> T2... 132 | ) -> Promise5, 133 | expect: (self: PromiseLike) -> T..., 134 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise5, 135 | getStatus: (self: PromiseLike) -> PromiseStatus, 136 | now: (self: PromiseLike, rejectionValue: any?) -> Promise4 137 | } 138 | type Promise3 = { 139 | andThen: ( 140 | self: PromiseLike, 141 | successHandler: ((T...) -> T2...)?, 142 | failureHandler: ((...any) -> T2...)? 143 | ) -> Promise4, 144 | andThenCall: ( 145 | self: PromiseLike, 146 | callback: (A...) -> T2..., 147 | A... 148 | ) -> Promise4, 149 | andThenReturn: (self: PromiseLike, T2...) -> Promise4, 150 | cancel: (self: PromiseLike) -> (), 151 | catch: ( 152 | self: PromiseLike, 153 | failureHandler: (...any) -> T2... 154 | ) -> Promise4, 155 | expect: (self: PromiseLike) -> T..., 156 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise4, 157 | getStatus: (self: PromiseLike) -> PromiseStatus, 158 | now: (self: PromiseLike, rejectionValue: any?) -> Promise3 159 | } 160 | type Promise2 = { 161 | andThen: ( 162 | self: PromiseLike, 163 | successHandler: ((T...) -> T2...)?, 164 | failureHandler: ((...any) -> T2...)? 165 | ) -> Promise3, 166 | andThenCall: ( 167 | self: PromiseLike, 168 | callback: (A...) -> T2..., 169 | A... 170 | ) -> Promise3, 171 | andThenReturn: (self: PromiseLike, T2...) -> Promise3, 172 | cancel: (self: PromiseLike) -> (), 173 | catch: ( 174 | self: PromiseLike, 175 | failureHandler: (...any) -> T2... 176 | ) -> Promise3, 177 | expect: (self: PromiseLike) -> T..., 178 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise3, 179 | getStatus: (self: PromiseLike) -> PromiseStatus, 180 | now: (self: PromiseLike, rejectionValue: any?) -> Promise2 181 | } 182 | type Promise1 = { 183 | andThen: ( 184 | self: PromiseLike, 185 | successHandler: ((T...) -> T2...)?, 186 | failureHandler: ((...any) -> T2...)? 187 | ) -> Promise2, 188 | andThenCall: ( 189 | self: PromiseLike, 190 | callback: (A...) -> T2..., 191 | A... 192 | ) -> Promise2, 193 | andThenReturn: (self: PromiseLike, T2...) -> Promise2, 194 | cancel: (self: PromiseLike) -> (), 195 | catch: ( 196 | self: PromiseLike, 197 | failureHandler: (...any) -> T2... 198 | ) -> Promise2, 199 | expect: (self: PromiseLike) -> T..., 200 | finally: (self: PromiseLike, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> Promise2, 201 | getStatus: (self: PromiseLike) -> PromiseStatus, 202 | now: (self: PromiseLike, rejectionValue: any?) -> Promise1 203 | } 204 | type _AnyPromise = { 205 | andThen: ( 206 | self: PromiseLike<...any>, 207 | successHandler: ((...any) -> T2...)?, 208 | failureHandler: ((...any) -> T2...)? 209 | ) -> any, 210 | andThenCall: ( 211 | self: PromiseLike<...any>, 212 | callback: (A...) -> T2..., 213 | A... 214 | ) -> any, 215 | andThenReturn: (self: PromiseLike<...any>, T2...) -> any, 216 | cancel: (self: PromiseLike<...any>) -> (), 217 | catch: ( 218 | self: PromiseLike<...any>, 219 | failureHandler: (...any) -> T2... 220 | ) -> any, 221 | expect: (self: PromiseLike<...any>) -> ...any, 222 | finally: (self: PromiseLike<...any>, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> any, 223 | getStatus: (self: PromiseLike<...any>) -> PromiseStatus, 224 | now: (self: PromiseLike<...any>, rejectionValue: any?) -> _AnyPromise 225 | } 226 | export type AnyPromise = _AnyPromise 227 | type PromiseLib = { 228 | Status: { 229 | Started: "Started", 230 | Resolved: "Resolved", 231 | Rejected: "Rejected", 232 | Cancelled: "Cancelled", 233 | }, 234 | 235 | all: (promises: { PromiseLike }) -> Promise1<{ T }>, 236 | delay: (seconds: number) -> Promise1, 237 | fromEvent: (event: RBXScriptSignal, predicate: ((T...) -> boolean)?) -> Promise1, 238 | new: (( 239 | resolve: (T...) -> (), 240 | reject: (...any) -> (), 241 | cancel: ((callback: (() -> ())?) -> boolean) -> () 242 | ) -> ()) -> Promise1, 243 | resolve: (T...) -> Promise1, 244 | try: (callback: (A...) -> T..., A...) -> Promise1, 245 | } 246 | -- stylua: ignore end 247 | 248 | return require(game:GetService("ReplicatedStorage").Packages.UntypedPromise) :: PromiseLib 249 | --------------------------------------------------------------------------------