├── .gitignore ├── .vscode └── settings.json ├── README.md ├── babel.config.js ├── package.json ├── packages ├── number │ ├── README.md │ ├── fix-macro-dts.js │ ├── macro │ │ └── package.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── macro │ │ │ └── index.ts │ │ └── types.ts │ └── tests │ │ ├── index.test.ts │ │ └── macro │ │ └── index.test.ts ├── predicate │ ├── README.md │ ├── fix-macro-dts.js │ ├── macro │ │ └── package.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── macro │ │ │ └── index.ts │ │ └── types.ts │ └── tests │ │ ├── .gitignore │ │ ├── index.test.ts │ │ ├── jest-expect-extras.ts │ │ ├── macro │ │ └── index.test.ts │ │ └── types.twoslash-test.ts └── runtime-checker │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── tests │ └── index.test.ts ├── tsconfig.json ├── twoslash-tester └── generate.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sthir 2 | 3 | The place where TypeScript gets leveraged like nowhere else. 4 | 5 | ## Packages 6 | 7 | - [`@sthir/predicate`](https://github.com/devanshj/sthir/tree/main/packages/predicate) — An eDSL to write typed predicates 8 | - [`@sthir/runtime-checker`](https://github.com/devanshj/sthir/tree/main/packages/runtime-checker) — Parsers to perform runtime type checking 9 | - [`@sthir/number`](https://github.com/devanshj/sthir/tree/main/packages/number) — Functions and types to work with numbers 10 | - More coming soon! 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sthir", 3 | "version": "0.0.1", 4 | "private": true, 5 | "main": "index.js", 6 | "license": "MIT", 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "devDependencies": { 11 | "@babel/core": "^7.26.10", 12 | "@babel/preset-env": "^7.26.9", 13 | "@babel/preset-typescript": "^7.27.0", 14 | "@preconstruct/cli": "^2.8.12", 15 | "@types/jest": "^29.5.14", 16 | "@types/node": "^22.13.14", 17 | "@typescript/twoslash": "^3.2.9", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.3.0", 20 | "ts-node": "^10.9.2", 21 | "typescript": "^5.8.2" 22 | }, 23 | "preconstruct": { 24 | "packages": [ 25 | "packages/*" 26 | ] 27 | }, 28 | "scripts": { 29 | "postinstall": "preconstruct dev" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/number/README.md: -------------------------------------------------------------------------------- 1 | # `@sthir/number` 2 | 3 | [![npm](https://img.shields.io/npm/v/@sthir/number?labelColor=000000&color=cb3837)](https://npm.im/@sthir/number) [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg)](https://github.com/kentcdodds/babel-plugin-macros) 4 | 5 | Function and types to work with numbers. Currently only includes `e` and `E` to facilitate `@sthir/predicate`'s `&` comparator usage, but will include more functions and types in future. 6 | 7 | ☝🏻 Note: It requires typescript version 4.8 and higher 8 | 9 | ## `e` 10 | 11 | An eDSL to write numeric-expressions that evaluate compile-time to produce more complete and narrow types. 12 | 13 | ```ts 14 | import * as N from "@sthir/number" 15 | 16 | let a: 0b01 = 0b01 17 | let b: 0b10 = 0b10 18 | 19 | let c: 0b11 = a | b 20 | // doesn't compile: Type 'number' is not assignable to type '3'. 21 | 22 | let d: 0b11 = N.e(`${a} | ${b}`) 23 | // compiles 24 | ``` 25 | 26 | You can also use the macro version in `@sthir/number/macro` that uses [`babel-plugin-macro`](https://github.com/kentcdodds/babel-plugin-macros) to transform ``N.e(`${a} | ${b}`)`` into `a | b` for zero runtime overhead. 27 | 28 | Supported operators are `&`, `|`, `<<`. We can have more operators but currently only these have a compelling use-case for using bitflag predicates with `@sthir/predicate`. If you have compelling use-cases for other operators feel free to open an issue. 29 | 30 | ## `E` 31 | 32 | Type-level version of `e` 33 | 34 | ```ts 35 | import * as N from "@sthir/number" 36 | 37 | type A = 0b01 38 | type B = 0b10 39 | type C = A | B 40 | // C is `0b01 | 0b10` because `|` is union not bitwise or 41 | 42 | type D = N.E<`${A} | ${B}`> 43 | // D is `0b11` 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/number/fix-macro-dts.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const fs = require("fs") 4 | const path = require("path") 5 | 6 | fs.writeFileSync( 7 | path.join(__dirname, "dist", "declarations", "src", "macro", "index.d.ts"), 8 | fs.readFileSync(path.join(__dirname, "dist", "declarations", "src", "index.d.ts"), "utf-8") 9 | .replace("./types", "../types"), 10 | "utf-8" 11 | ) -------------------------------------------------------------------------------- /packages/number/macro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/sthir-number-macro.cjs.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sthir/number", 3 | "version": "0.0.2", 4 | "description": "Functions and types to work with numbers", 5 | "main": "dist/sthir-number.cjs.js", 6 | "preconstruct": { 7 | "entrypoints": [ 8 | "index.ts", 9 | "macro/index.ts" 10 | ] 11 | }, 12 | "author": { 13 | "name": "Devansh Jethmalani", 14 | "email": "jethmalani.devansh@gmail.com" 15 | }, 16 | "license": "MIT", 17 | "readme": "https://github.com/devanshj/sthir/tree/main/packages/number/README.md", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/devanshj/sthir.git" 21 | }, 22 | "scripts": { 23 | "build": "cd ../../ && preconstruct build", 24 | "postbuild": "node fix-macro-dts", 25 | "test": "jest" 26 | }, 27 | "jest": { 28 | "preset": "ts-jest", 29 | "testEnvironment": "node" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.26.10", 33 | "@types/babel-plugin-macros": "^3.1.3" 34 | }, 35 | "dependencies": { 36 | "babel-plugin-macros": "^3.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/number/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { E, ERuntime } from "./types" 2 | 3 | export { E } 4 | 5 | export const e = (x => { 6 | try { return (new Function(`return ${x}`))() } 7 | catch (error) { 8 | if (!(error instanceof EvalError)) throw error 9 | return _e(x) 10 | } 11 | }) as ERuntime 12 | 13 | const _e = (x: string): number => { 14 | let r = ([ 15 | [/(.*)\((.*)\)(.*)/, m => _e(`${m[1]}${_e(m[2]!)}${m[3]}`)], 16 | [/(.*) \& (.*)/, m => _e(m[1]!) & _e(m[2]!)], 17 | [/(.*) \| (.*)/, m => _e(m[1]!) | _e(m[2]!)], 18 | [/(.*) \<\< (.*)/, m => _e(m[1]!) << _e(m[2]!)], 19 | [/0b(.*)/, m => parseInt(m[1]!, 2)], 20 | [/(.*)/, m => parseInt(m[1]!, 10)], 21 | ] as [RegExp, (x: RegExpMatchArray) => number][]) 22 | .flatMap(([p, f]) => { 23 | let m = x.match(p) 24 | if (m) return f(m) 25 | return [] 26 | })[0] 27 | 28 | if (!r) throw new Error(`Cannot parse expression '${x}'`) 29 | return r 30 | } 31 | -------------------------------------------------------------------------------- /packages/number/src/macro/index.ts: -------------------------------------------------------------------------------- 1 | import type { types as bt, NodePath as btNodePath } from "@babel/core" 2 | import { createMacro, MacroError } from "babel-plugin-macros" 3 | 4 | export default createMacro(({ references, babel: { types: bt, parse, traverse } }) => { 5 | const main = () => doAndMapStringError(() => { 6 | transformEReferences(references.e ?? []) 7 | }, e => new MacroError(e)) 8 | 9 | // ---------- 10 | // e 11 | 12 | const transformEReferences = (refs: btNodePath[]) => { 13 | for (let path of refs.map(r => r.parentPath)) 14 | path?.replaceWith(eMacro(...parseEArguments(path.node))) 15 | } 16 | 17 | const eMacro = (...[a]: EArguments) => { 18 | if (bt.isStringLiteral(a)) return parseExpression(a.value) 19 | 20 | let n = parseExpression(a.quasis.map(q => q.value.raw).join("$")) 21 | let i = 0 22 | traverse(n, { 23 | Identifier: n => (n.replaceWith(a.expressions[i]!), i++), 24 | noScope: true 25 | }) 26 | return n 27 | } 28 | 29 | type EArguments = 30 | [bt.StringLiteral | bt.TemplateLiteral] 31 | 32 | const parseEArguments = (node: bt.Node) => doAndMapStringError(() => { 33 | if (!bt.isCallExpression(node)) throw "`e` was expected to be called" 34 | 35 | let as = node.arguments 36 | if (!(as.length === 1)) throw "`e` expects 1 argument" 37 | 38 | if (!(bt.isStringLiteral(as[0]) || bt.isTemplateLiteral(as[0]))) { 39 | throw "`e` expects a string or template literal as the first argument" 40 | } 41 | 42 | return as as EArguments 43 | }, e => `${e}, at ${loc(node)}`) 44 | 45 | 46 | 47 | // ---------- 48 | // extras 49 | 50 | const parseExpression = (code: string) => { 51 | let file = parse(code, { filename: "temp.js" }) as bt.File 52 | return (file.program.body[0]! as bt.ExpressionStatement).expression 53 | } 54 | 55 | const doAndMapStringError = (e: () => R, f: (e: string) => unknown) => { 56 | try { return e() } 57 | catch (e) { 58 | if (typeof e !== "string") throw e 59 | throw f(e) 60 | } 61 | } 62 | 63 | const loc = (node: bt.Node) => { 64 | if (!node) return ":" 65 | let start = node.loc?.start 66 | if (!start) return ":" 67 | return start.line + ":" + start.column; 68 | } 69 | 70 | return main() 71 | }) 72 | -------------------------------------------------------------------------------- /packages/number/src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type ERuntime = 3 | < T extends [e: R extends `Error: ${string}` ? R : T[0]] 4 | , R = E 5 | > 6 | (...e: T extends [string] ? T : [e: string]) => 7 | R extends `Error: ${string}` 8 | ? number 9 | : R 10 | 11 | export type E = 12 | string extends T ? number : 13 | T extends `${infer X}(${infer Y})${infer Z}` 14 | ? E extends infer Ey 15 | ? Ey extends `Error: ${string}` ? Ey : E<`${X}${Ey & number}${Z}`> 16 | : never : 17 | T extends `${infer X} & ${infer Y}` 18 | ? [E, E] extends [infer Ex, infer Ey] 19 | ? Ex extends `Error: ${string}` ? Ex : 20 | Ey extends `Error: ${string}` ? Ey : 21 | N._And 22 | : never : 23 | T extends `${infer X} | ${infer Y}` 24 | ? [E, E] extends [infer Ex, infer Ey] 25 | ? Ex extends `Error: ${string}` ? Ex : 26 | Ey extends `Error: ${string}` ? Ey : 27 | N._Or 28 | : never : 29 | T extends `${infer X} << ${infer Y}` 30 | ? [E, E] extends [infer Ex, infer Ey] 31 | ? Ex extends `Error: ${string}` ? Ex : 32 | Ey extends `Error: ${string}` ? Ey : 33 | N._LeftShift 34 | : never : 35 | T extends `0b${infer X}` ? Nb.ToNumber, Nb.Unknown>> : 36 | T extends `${infer X extends number}` ? X : 37 | `Error: Cannot parse expression '${T}'` 38 | 39 | type Test0 = A.Test, 5>> 40 | type Test1 = A.Test, 2>> 41 | type Test2 = A.Test, 0b10>> 42 | type Test3 = A.Test, 0b10>> 43 | type Test4 = A.Test, 0b11>> 44 | type Test5 = A.Test, 0b11>> 45 | type Test6 = A.Test, 0b10100>> 46 | type Test7 = A.Test, 0b10100>> 47 | type Test8 = A.Test, 0b10110>> 48 | type Test9 = A.Test, 0b11100>> 49 | type Test10 = A.Test, 0b101 | 0b110>> 50 | type Test11 = A.Test, number>> 51 | 52 | namespace N { 53 | export type _And = 54 | Nb._ToNumber, Nb._FromNumber>> 55 | 56 | export type _Or = 57 | Nb._ToNumber, Nb._FromNumber>> 58 | 59 | export type _LeftShift = 60 | Nb._ToNumber, Nb._FromNumber>> 61 | } 62 | 63 | namespace Nd { 64 | export type Unknown = ("0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")[] 65 | type Digit = Unknown[number] 66 | type EvenDigit = "0" | "2" | "4" | "6" | "8" 67 | type OddDigit = U.Exclude 68 | 69 | // https://en.wikipedia.org/wiki/Division_by_two#Decimal 70 | export type DivideByTwoFloor = 71 | IsFirstCheck extends true 72 | ? DivideByTwoFloor<["0", ...Dividend], false> extends infer R 73 | ? R extends ["0"] ? R : 74 | R extends ["0", ...infer X] ? X : 75 | R 76 | : never : 77 | Dividend extends [] ? [] : 78 | [ ...( 79 | Dividend extends [EvenDigit, "0" | "1", ...Digit[]] ? ["0"] : 80 | Dividend extends [EvenDigit, "2" | "3", ...Digit[]] ? ["1"] : 81 | Dividend extends [EvenDigit, "4" | "5", ...Digit[]] ? ["2"] : 82 | Dividend extends [EvenDigit, "6" | "7", ...Digit[]] ? ["3"] : 83 | Dividend extends [EvenDigit, "8" | "9", ...Digit[]] ? ["4"] : 84 | Dividend extends [OddDigit, "0" | "1", ...Digit[]] ? ["5"] : 85 | Dividend extends [OddDigit, "2" | "3", ...Digit[]] ? ["6"] : 86 | Dividend extends [OddDigit, "4" | "5", ...Digit[]] ? ["7"] : 87 | Dividend extends [OddDigit, "6" | "7", ...Digit[]] ? ["8"] : 88 | Dividend extends [OddDigit, "8" | "9", ...Digit[]] ? ["9"] : 89 | [] 90 | ) 91 | , ...DivideByTwoFloor, Unknown>, false> 92 | ] 93 | 94 | export type DivideByTwo = 95 | { quotient: DivideByTwoFloor 96 | , remainder: Dividend extends [...Digit[], OddDigit] ? ["1"] : ["0"] 97 | } 98 | 99 | type Test0 = A.Test, 101 | { quotient: ["8", "6", "9"], remainder: ["1"] } 102 | >> 103 | 104 | export type MultiplyByTwo = 105 | T extends [Digit] 106 | ? T extends ["0"] ? AddOne extends false ? ["0"] : ["1"] : 107 | T extends ["1"] ? AddOne extends false ? ["2"] : ["3"] : 108 | T extends ["2"] ? AddOne extends false ? ["4"] : ["5"] : 109 | T extends ["3"] ? AddOne extends false ? ["6"] : ["7"] : 110 | T extends ["4"] ? AddOne extends false ? ["8"] : ["9"] : 111 | T extends ["5"] ? AddOne extends false ? ["1", "0"] : ["1", "1"] : 112 | T extends ["6"] ? AddOne extends false ? ["1", "2"] : ["1", "3"] : 113 | T extends ["7"] ? AddOne extends false ? ["1", "4"] : ["1", "5"] : 114 | T extends ["8"] ? AddOne extends false ? ["1", "6"] : ["1", "7"] : 115 | T extends ["9"] ? AddOne extends false ? ["1", "8"] : ["1", "9"] : 116 | never 117 | : T extends [...infer H extends Digit[], infer T extends Digit] 118 | ? MultiplyByTwo<[T], AddOne> extends infer R 119 | ? R extends [infer X] ? [...MultiplyByTwo, X] : 120 | R extends ["1", infer X] ? [...MultiplyByTwo, X] : 121 | never 122 | : never 123 | : never 124 | 125 | export type AddOne = 126 | T extends [Digit] 127 | ? T extends ["0"] ? ["1"] : 128 | T extends ["1"] ? ["2"] : 129 | T extends ["2"] ? ["3"] : 130 | T extends ["3"] ? ["4"] : 131 | T extends ["4"] ? ["5"] : 132 | T extends ["5"] ? ["6"] : 133 | T extends ["6"] ? ["7"] : 134 | T extends ["7"] ? ["8"] : 135 | T extends ["8"] ? ["9"] : 136 | T extends ["9"] ? ["1", "0"] : 137 | never 138 | : T extends [...infer H extends Digit[], infer T extends Digit] 139 | ? AddOne<[T]> extends infer R 140 | ? R extends [infer X] ? [...H, X] : 141 | R extends ["1", infer X] ? [...AddOne, X] : 142 | never 143 | : never 144 | : never 145 | } 146 | 147 | export namespace Nb { 148 | export type Unknown = ("0" | "1")[] 149 | type Digit = Unknown[number] 150 | 151 | export type And = 152 | Unknown extends A ? Unknown : 153 | Unknown extends B ? Unknown : 154 | keyof A extends keyof B 155 | ? _TrimLeadingZeros, B>> 156 | : And 157 | 158 | export type _And = 159 | And, A.Cast> 160 | 161 | export type AndPadded = 162 | [...A, ...B] extends ["0", "0"] ? ["0"] : 163 | [...A, ...B] extends ["0", "1"] ? ["0"] : 164 | [...A, ...B] extends ["1", "0"] ? ["0"] : 165 | [...A, ...B] extends ["1", "1"] ? ["1"] : 166 | { [I in keyof B]: 167 | AndPadded<[A.Cast], [A.Cast, Digit>]>[0] 168 | } 169 | 170 | type Test0 = A.Test, Nb.FromNumber<0b10>>, ["0"]>> 171 | type Test1 = A.Test, Nb.FromNumber<0b100>>, ["0"]>> 172 | type Test2 = A.Test, Nb.FromNumber<0b110>>, ["1", "0", "0"]>> 173 | 174 | 175 | export type Or = 176 | Unknown extends A ? Unknown : 177 | Unknown extends B ? Unknown : 178 | keyof A extends keyof B 179 | ? _TrimLeadingZeros, B>> 180 | : Or 181 | 182 | export type _Or = 183 | Or, A.Cast> 184 | 185 | export type OrPadded = 186 | [...A, ...B] extends ["0", "0"] ? ["0"] : 187 | [...A, ...B] extends ["0", "1"] ? ["1"] : 188 | [...A, ...B] extends ["1", "0"] ? ["1"] : 189 | [...A, ...B] extends ["1", "1"] ? ["1"] : 190 | { [I in keyof B]: 191 | OrPadded<[A.Cast], [A.Cast, Digit>]>[0] 192 | } 193 | 194 | type Test3 = A.Test, Nb.FromNumber<0b01>>, ["1", "1"]>> 195 | 196 | 197 | export type LeftShift = 198 | Unknown extends A ? Unknown : 199 | Unknown extends B ? Unknown : 200 | B extends ["0"] 201 | ? A 202 | : LeftShift<[...A, "0"], Decrement> 203 | 204 | export type _LeftShift = 205 | LeftShift, A.Cast> 206 | 207 | // https://www.geeksforgeeks.org/subtract-1-without-arithmetic-operators/ 208 | type Decrement = 209 | A extends [] ? [] : 210 | SplitAtRightmostOne extends infer R extends [Unknown, Unknown] 211 | ? TrimLeadingZeros<[...R[0], "0", ...A.Cast, Unknown>]> 212 | : never 213 | 214 | type SplitAfterRightmostOne = 215 | A extends [infer X extends Digit, ...infer Y extends Unknown] 216 | ? "1" extends Y[number] 217 | ? SplitAfterRightmostOne extends infer R extends [Unknown, Unknown] 218 | ? [[X, ...R[0]], R[1]] 219 | : never 220 | : [[X], Y] 221 | : [] 222 | 223 | type SplitAtRightmostOne = 224 | SplitAfterRightmostOne extends infer R extends [Unknown, Unknown] 225 | ? [L.Popped, R[1]] 226 | : [[], []] 227 | 228 | type Not = 229 | { [I in keyof A]: A[I] extends "0" ? "1" : "0" 230 | } 231 | 232 | type Test4 = A.Test, ["0"]>> 233 | type Test5 = A.Test, ["1", "0", "0", "0", "1"]>> 234 | type Test6 = A.Test, ["0"]>> 235 | type Test7 = A.Test, []>> 236 | 237 | 238 | export type FromNumber = 239 | number extends T ? Nb.Unknown : 240 | A.Cast, Nd.Unknown>>, Nb.Unknown> 241 | 242 | export type _FromNumber = 243 | FromNumber> 244 | 245 | type FromDecimal = 246 | Nd.DivideByTwo extends { quotient: infer Q, remainder: infer R } 247 | ? { 0: R 248 | , 1: [...FromDecimal>, ...A.Cast] 249 | }[Q extends ["0"] ? 0 : 1] 250 | : never 251 | 252 | type Test8 = A.Test, ["1", "0", "1"]>> 253 | type Test9 = A.Test, ["1", "0", "0", "0", "0"]>> 254 | 255 | 256 | type TrimLeadingZeros = 257 | T extends ["0"] ? T : 258 | T extends ["0", ...infer X extends Unknown] ? TrimLeadingZeros : 259 | T 260 | 261 | type _TrimLeadingZeros = 262 | TrimLeadingZeros> 263 | 264 | type PadZerosSameAs = 265 | A["length"] extends B["length"] ? A : 266 | PadZerosSameAs<["0", ...A], B> 267 | 268 | 269 | type ToDecimal = 270 | T extends [] ? P : 271 | T extends [infer H extends Digit, ...infer T extends Digit[]] 272 | ? H extends "0" ? ToDecimal> : 273 | H extends "1" ? ToDecimal>> : 274 | never 275 | : never 276 | 277 | 278 | export type ToNumber = 279 | Unknown extends T ? number : 280 | L.Join, ""> extends `${infer X extends number}` ? X : never 281 | 282 | export type _ToNumber = 283 | ToNumber> 284 | 285 | type Test10 = A.Test, 5>> 286 | type Test11 = A.Test, 16>> 287 | type Test12 = A.Test, 3>> 288 | } 289 | 290 | 291 | namespace L { 292 | export type Join = 293 | L extends [] ? "" : 294 | L extends [infer Lh] ? A.Cast : 295 | L extends [infer Lh, ...infer Lt] 296 | ? `${A.Cast}${A.Cast}${L.Join}` : 297 | never 298 | 299 | export type Shifted = 300 | L extends [] ? [] : 301 | L extends [unknown, ...infer T] ? T : 302 | never 303 | 304 | export type Popped = 305 | L extends [] ? [] : 306 | L extends [...infer T, unknown] ? T : 307 | never 308 | 309 | export type Reverse = 310 | L extends [] ? [] : 311 | L extends [infer H, ...infer T] ? [...Reverse, H] : 312 | never 313 | } 314 | 315 | namespace S { 316 | export type Split = 317 | S extends "" ? [] : 318 | S extends `${infer Sh}${A.Cast}${infer St}` 319 | ? [Sh, ...Split] : 320 | [S] 321 | } 322 | 323 | namespace A { 324 | export type Get = 325 | B.Not> extends true ? Get : 326 | P extends [] ? T : 327 | P extends [infer Ph] ? Ph extends keyof T ? T[Ph] : undefined : 328 | P extends [infer Ph, ...infer Pr] ? Get, Pr> : 329 | never 330 | 331 | export type DoesExtend = 332 | A extends B ? true : false 333 | 334 | export type Cast = 335 | T extends U ? T : U 336 | 337 | export type Templateable = 338 | | string 339 | | number 340 | | boolean 341 | | null 342 | | undefined 343 | | bigint 344 | 345 | export type AreEqual = 346 | (() => T extends B ? 1 : 0) extends (() => T extends A ? 1 : 0) 347 | ? true 348 | : false; 349 | 350 | export type Test = T 351 | export const areEqual = 352 | (_debug?: (value: A) => void) => 353 | undefined as unknown as A.AreEqual 354 | } 355 | 356 | namespace U { 357 | export type Exclude = 358 | T extends U ? never : T 359 | } 360 | 361 | namespace B { 362 | export type Not = 363 | T extends true ? false : true 364 | } 365 | -------------------------------------------------------------------------------- /packages/number/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { e } from "../src" 2 | 3 | describe("e", () => { 4 | it("uses `new Function`", () => { 5 | expect(e("0b01 | 0b10")).toBe(0b11) 6 | }) 7 | 8 | it("uses parser in case of EvalError", () => { 9 | let savedFunction = global.Function; 10 | global.Function = function() { 11 | return () => { 12 | throw new EvalError() 13 | } 14 | } as any 15 | 16 | expect(() => { 17 | (new Function("return 0b01 | 0b10"))() 18 | }).toThrowError(EvalError) 19 | 20 | expect(e("0b01 | 0b10")).toBe(0b11) 21 | 22 | global.Function = savedFunction 23 | }) 24 | 25 | it("doesn't compile for invalid expressions", () => { 26 | expect(() => { 27 | // @ts-expect-error 28 | e("0b01 | lol") 29 | }).toThrowError(ReferenceError) 30 | }) 31 | }) -------------------------------------------------------------------------------- /packages/number/tests/macro/index.test.ts: -------------------------------------------------------------------------------- 1 | import tester from "babel-plugin-tester" 2 | import macros from "babel-plugin-macros" 3 | 4 | tester({ 5 | plugin: macros, 6 | formatResult: a => a, 7 | pluginName: "@sthir/number/macro", 8 | tests: { 9 | "works": { 10 | code: ` 11 | import { e } from "@sthir/number/macro"; 12 | 13 | let x = e(\`\${(() => 1)()} & 2\`); 14 | `, 15 | output: ` 16 | let x = (() => 1)() & 2; 17 | ` 18 | } 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/predicate/README.md: -------------------------------------------------------------------------------- 1 | # `@sthir/predicate` 2 | 3 | [![npm](https://img.shields.io/npm/v/@sthir/predicate?labelColor=000000&color=cb3837)](https://npm.im/@sthir/predicate) [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg)](https://github.com/kentcdodds/babel-plugin-macros) 4 | 5 | Write inferred type predicates... 6 | 7 | - With the `p` eDSL... See [this twitter thread](https://twitter.com/devanshj__/status/1477950624343871488) for an introduction. 8 | - With `pm`... See [this twitter thread](https://twitter.com/devanshj__/status/1585014432379199488) for an introduction. 9 | 10 | ## Exports 11 | 12 | ```ts 13 | // `p` is short for `predicate` 14 | // (this is a pseudo type) 15 | export const p: 16 | < T 17 | , OsCor extends Join<[...Operator[], Comparator?], " "> 18 | , Cnd extends (HasComparator extends true ? [Comparand] : []) 19 | > 20 | ( operatorsMaybeComparator?: OsCor 21 | , ...comparand: Cnd 22 | ) => 23 | (operand: T) => 24 | operand is Narrow 25 | 26 | // `pm` is short for `predicateFromMaybe` 27 | export const pm: 28 | 29 | (f: (t: T) => [T] | []) => 30 | (t: T) => t is U 31 | 32 | // `pa` is short for `predicateApply` 33 | export const pa: 34 | 35 | (operand: T, predicate: (t: T) => t is U) => 36 | operand is U 37 | ``` 38 | 39 | ## `p` 40 | 41 | ```ts 42 | import { p } from "@sthir/predicate" 43 | 44 | declare let xs: 45 | (undefined | { a: string | number } | { b: string })[] 46 | 47 | 48 | // Without @sthir/predicate ... 49 | 50 | xs 51 | .filter(x => typeof x?.a === "string") 52 | // ~ 53 | // Property 'a' does not exist on type '{ a: string | number; } | { b: string; }'. 54 | // Property 'a' does not exist on type '{ b: string; }' 55 | .map(x => x.a.toUpperCase()) 56 | // ~ 57 | // Object is possibly 'undefined' 58 | // ~ 59 | // Property 'a' does not exist on type '{ a: string | number; } | { b: string; }'. 60 | // Property 'a' does not exist on type '{ b: string; }' 61 | 62 | 63 | 64 | // With @sthir/predicate's p ... 65 | 66 | xs 67 | .filter(p("?.a typeof ===", "string")) 68 | .map(x => x.a.toUpperCase()) 69 | ``` 70 | 71 | ```ts 72 | import { pa, p } from "@sthir/predicate" 73 | 74 | declare let foo: 75 | | { bar: { type: "x" } 76 | , x: number 77 | } 78 | | { bar: { type: "y" } 79 | , y: number 80 | } 81 | 82 | // Without @sthir/predicate ... 83 | 84 | if (foo.bar.type === "x") { 85 | foo.x 86 | // ~ 87 | // Property 'x' does not exist on type '{ bar: { type: "x"; }; x: number; } | { bar: { type: "y"; }; y: number; }'. 88 | // Property 'x' does not exist on type '{ bar: { type: "y"; }; y: number; }' 89 | } 90 | 91 | // With @sthir/predicate's p... 92 | 93 | if (pa(foo, p(".bar.type ===", "x"))) { 94 | foo.x 95 | } 96 | ``` 97 | 98 | ### Macro 99 | 100 | You can use the macro version with [`babel-plugin-macros`](https://github.com/kentcdodds/babel-plugin-macros) 101 | 102 | ```ts 103 | import { p, pa } from "@sthir/predicate/macro"; 104 | 105 | pa(x, p(".a?.b", "typeof", "===", y)); 106 | ``` 107 | 108 | Gets transformed in build-time to 109 | 110 | ```ts 111 | (t => typeof t.a?.b === y)(x); 112 | ``` 113 | 114 | #### Supported operators 115 | 116 | - Index (`.a`, `?.a`, `.a.b`, `.a?.b`, etc) 117 | - `typeof` (postfix) 118 | - `` `&${x}` `` (without space, requires typescript version 4.8 and higher). See [this thread](https://twitter.com/devanshj__/status/1551972693053829120?t=F4wMtKuy-oCOML8iQLZytQ&s=19) for suggested usage. 119 | 120 | #### Supported comparators 121 | 122 | - `===` 123 | - `!==` 124 | - Implicit/Truthy eg `p(".a")`, same as `!== Falsy` 125 | 126 | #### Pragmatic choices regarding `!==` 127 | 128 | Generally speaking (not just for this library), it's good to avoid `!==`, because `!==` introduces a "negative requirement" which is unintuitive. Let me give you an example... 129 | 130 | ```ts 131 | const foo = (x: { a?: string } | { b: string }) => { 132 | if ("a" in x && x.a !== undefined) { 133 | x.a; // string | undefined 134 | (x.a as string).toUpperCase(); 135 | } 136 | } 137 | ``` 138 | 139 | Now you might think that the above assertion `as string` is safe, but it's actually not, the following code compiles... 140 | 141 | ```ts 142 | let x = { b: "", a: 0 } 143 | let y: { b: string } = x 144 | foo(y) // Uncaught TypeError: x.a.toUpperCase is not a function 145 | ``` 146 | 147 | You see you forgot that `{ b: string, a: number }` is a subtype of `{ b: string }` so just because you can's see `"a"` in `{ b: string }` doesn't mean there is no `"a"` in it, `{ b: string }` is mathematically same as `{ b: string, a: unknown }`. So just because you checked `a` is not `undefined` doesn't mean it'll be `string`. 148 | 149 | But a lot of usual js patterns use `!==` checks (which includes truthy checks because that's same as `!== falsy`) so `@sthir/predicate` makes a pragmatic choice and assumes you're not doing something funny. In fact TypeScript also assumes that because narrowing `x.a` to `string | undefined` is also incorrect as it could be `number` too as we saw above. 150 | 151 | ```ts 152 | const foo = (x: { a?: string } | { b: string }) => { 153 | if (pa(x, p(".a !==", undefined)) { 154 | x.a; // string 155 | x.a.toUpperCase(); 156 | } 157 | } 158 | ``` 159 | 160 | Usually it's not a big deal, it's okay to use `!==`, semantics are important, `if (x !== undefined)` reads way better than `if (typeof x === 'string')`, just don't unnecessarily use `!==`. 161 | 162 | #### Future 163 | 164 | - Numeric operators (`>`, `<=`, etc). One of the major use cases would be doing a `pa(xs, p(".length >=", 1))` would narrow `xs` from `T[]` to `[T, ...T[]]`. 165 | - Call operator `(...)`. 166 | ```ts 167 | declare const a: 168 | | { isFoo: (x: number) => true, foo: string } 169 | | { isFoo: (x: number) => false, foo: undefined } 170 | 171 | if (pa(a, p(".isFoo (", 10, ")")) { 172 | a.foo.toUpperCase(); 173 | } 174 | ``` 175 | - Maybe more 176 | 177 | ## `pm` 178 | 179 | Like `p` except it relies on TypeScript's narrowing algorithm, which is less complete than `p`'s. 180 | 181 | ```ts 182 | import { pm } from "@sthir/predicate" 183 | 184 | declare let xs: 185 | (string | undefined)[] 186 | 187 | // Without @sthir/predicate's pm ... 188 | xs 189 | .filter(x => x !== undefined) 190 | .map(x => x.toUpperCase()) 191 | // ~ 192 | // Object is possibly undefined. 193 | 194 | 195 | // With @sthir/predicate's pm ... 196 | xs 197 | .filter(pm(x => x !== undefined ? [x] : [])) 198 | .map(x => x.toUpperCase()) 199 | ``` 200 | 201 | ### Versioning notes 202 | 203 | Note that the current version has a zero major, which means there can be breaking changes without a major bump. 204 | 205 | > Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable. 206 | > 207 | > — [_semver.org_](https://semver.org/#spec-item-4) 208 | 209 | You can still use it of course, just be careful before bumping to a newer version. 210 | -------------------------------------------------------------------------------- /packages/predicate/fix-macro-dts.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const fs = require("fs") 4 | const path = require("path") 5 | 6 | fs.writeFileSync( 7 | path.join(__dirname, "dist", "declarations", "src", "macro", "index.d.ts"), 8 | fs.readFileSync(path.join(__dirname, "dist", "declarations", "src", "index.d.ts"), "utf-8") 9 | .replace("./types", "../types"), 10 | "utf-8" 11 | ) -------------------------------------------------------------------------------- /packages/predicate/macro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/sthir-predicate-macro.cjs.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/predicate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sthir/predicate", 3 | "version": "0.22.0", 4 | "description": "Write inferred type predicates", 5 | "main": "dist/sthir-predicate.cjs.js", 6 | "preconstruct": { 7 | "entrypoints": [ 8 | "index.ts", 9 | "macro/index.ts" 10 | ] 11 | }, 12 | "author": { 13 | "name": "Devansh Jethmalani", 14 | "email": "jethmalani.devansh@gmail.com" 15 | }, 16 | "license": "MIT", 17 | "readme": "https://github.com/devanshj/sthir/tree/main/packages/predicate/README.md", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/devanshj/sthir.git" 21 | }, 22 | "scripts": { 23 | "pretest": "node ../../twoslash-tester/generate tests/types.twoslash-test.ts", 24 | "test": "jest", 25 | "build": "cd ../../ && preconstruct build", 26 | "postbuild": "node fix-macro-dts" 27 | }, 28 | "jest": { 29 | "preset": "ts-jest", 30 | "testEnvironment": "node" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.26.10", 34 | "@types/babel-plugin-macros": "^3.1.3", 35 | "babel-plugin-macros": "^3.1.0", 36 | "babel-plugin-tester": "^11.0.4" 37 | }, 38 | "dependencies": { 39 | "@sthir/number": "^0.0.2", 40 | "babel-plugin-macros": "^3.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/predicate/src/index.ts: -------------------------------------------------------------------------------- 1 | export { p, pa, pm } 2 | 3 | import { P, Pa, Pm } from "./types" 4 | 5 | // ---------- 6 | // p 7 | 8 | type PImpl = 9 | (...a: [] | [Operators] | [OperatorsComparator, Comparand]) => 10 | (operand: Operand) => boolean 11 | 12 | type Operand = unknown & { __isOperand: true } 13 | type Operators = string & { __isOperators: true } 14 | type OperatorsComparator = string & { __isOperatorsComparator: true } 15 | type Comparand = unknown & { __isComparand: true } 16 | 17 | const pImpl: PImpl = (...a) => t => { 18 | if (isEmpty(a)) return Boolean(t) 19 | 20 | let [_osCor, cnd] = a 21 | let osCor = _osCor.split(" ") as 22 | (`.${string}` | `?.${string}` | "typeof" | "===" | "!==" | `&${string}`)[] 23 | 24 | return osCor.reduce((v, x) => { 25 | if (doesStartWith(x, ".") || doesStartWith(x, "?.")) return get( 26 | v, 27 | x.replace(/\?\./g, ".").replace(/^\./g, "").split(".") 28 | ) 29 | if (x === "typeof") return typeof v 30 | if (x === "===") return v === (cnd as unknown) 31 | if (x === "!==") return v !== (cnd as unknown) 32 | if (doesStartWith(x, "&")) return (v as number) & Number(x.slice(1)) 33 | 34 | if (process.env.NODE_ENV === "development") { 35 | assertNever(x) 36 | } 37 | return v 38 | }, t as unknown) as boolean 39 | } 40 | 41 | const p = pImpl as P 42 | 43 | 44 | 45 | // --------- 46 | // pa 47 | 48 | const pa = 49 | ((t, p) => p(t)) as Pa 50 | 51 | 52 | 53 | // --------- 54 | // pt 55 | 56 | const pm = 57 | (f => t => f(t).length === 1) as Pm 58 | 59 | 60 | // ---------- 61 | // extras 62 | 63 | const get = (t: unknown, ks: string[]): unknown => { 64 | if (ks.length === 0) return t; 65 | let [k, ...ks_] = ks as [string, ...string[]] 66 | if (t === undefined || t === null) return t; 67 | return get((t as never)[k], ks_) 68 | } 69 | 70 | const isEmpty = (xs: T): xs is T & [] => 71 | xs.length === 0 72 | 73 | const doesStartWith = 74 | 75 | (s: S, h: H): s is S & `${H}${string}` => 76 | s.startsWith(h) 77 | 78 | const assertNever: (a?: never) => never = a => { 79 | throw new Error( 80 | "Invariant: `assertNever` called with " + 81 | JSON.stringify(a, null, " ") 82 | ) 83 | } -------------------------------------------------------------------------------- /packages/predicate/src/macro/index.ts: -------------------------------------------------------------------------------- 1 | import type { types as bt, NodePath as btNodePath } from "@babel/core" 2 | import { createMacro, MacroError } from "babel-plugin-macros" 3 | 4 | export default createMacro(({ references: _references, babel: { types: bt, parse, traverse } }) => { 5 | let references = _references as unknown as 6 | { [K in keyof typeof import("../")]: btNodePath[] } 7 | 8 | const main = () => doAndMapStringError(() => { 9 | transformP(references.p ?? []) 10 | transformPm(references.pm ?? []) 11 | transformPa(references.pa ?? []) 12 | }, e => new MacroError(e)) 13 | 14 | 15 | // ---------- 16 | // p 17 | 18 | const transformP = (refs: btNodePath[]) => { 19 | for (let path of refs.map(r => r.parentPath)) 20 | path?.replaceWith(pMacro(parsePArguments(path.node))) 21 | } 22 | 23 | const pMacro = ({ value: as, identifiers: $s }: PArguments) => { 24 | if (as.length === 0) { 25 | return bt.identifier("Boolean") 26 | } 27 | 28 | let operators = 29 | as.filter(a => 30 | typeof a === "string" && 31 | (isIndex(a) || isTypeof(a) || isBitwiseAnd(a)) 32 | ) as Operator[] 33 | 34 | let operation = 35 | parseExpression(operators.reduce((v, o) => { 36 | if (isIndex(o)) return `(${v}${o})` 37 | if (isTypeof(o)) return `(typeof ${v})` 38 | if (isBitwiseAnd(o)) return `(${v} ${o.replace("&", "& ")})` 39 | assertNever(o) 40 | }, "t")) 41 | 42 | fillIdentifiers(operation, $s) 43 | 44 | let comparator = 45 | as.find(a => 46 | typeof a === "string" && 47 | isComparator(a) 48 | ) as Comparator | undefined 49 | 50 | let comparand = 51 | as.find(a => 52 | typeof a === "object" && 53 | isComparand(a) 54 | ) as Comparand | undefined 55 | 56 | 57 | if (!comparand) { 58 | return bt.arrowFunctionExpression( 59 | [bt.identifier("t")], 60 | bt.callExpression(bt.identifier("Boolean"), [operation]) 61 | ) 62 | } 63 | 64 | return bt.arrowFunctionExpression( 65 | [bt.identifier("t")], 66 | bt.binaryExpression(comparator!, operation, comparand) 67 | ) 68 | } 69 | 70 | const fillIdentifiers = (destination: bt.Expression, identifiers: PArguments["identifiers"]) => { 71 | traverse(destination, { 72 | Identifier: n => { 73 | if (!n.node.name.startsWith("$")) return 74 | let i = Number(n.node.name.replace("$", "")) 75 | n.replaceWith(identifiers[i]!) 76 | }, 77 | noScope: true 78 | }) 79 | } 80 | 81 | type PArguments = 82 | { value: [...Operator[], ...([] | [Comparator, Comparand])] 83 | , identifiers: bt.Expression[] 84 | } 85 | 86 | type Operator = `${`?` | `.`}${string}` | "typeof" | `&${string}` 87 | type Comparator = "===" | "!==" 88 | type Comparand = bt.Expression 89 | 90 | const parsePArguments = (node: bt.Node) => doAndMapStringError(() => { 91 | if (!bt.isCallExpression(node)) throw "`p` was expected to be called" 92 | 93 | let as = node.arguments 94 | if (as.length === 0) return { value: [], identifiers: [] } as PArguments 95 | 96 | if (!(bt.isStringLiteral(as[0]) || bt.isTemplateLiteral(as[0]))) { 97 | throw "Expected a string or template literal, at first argument" 98 | } 99 | let osCor = 100 | bt.isStringLiteral(as[0]) ? as[0].value.split(" ") : 101 | as[0].quasis.map(q => q.value.raw) 102 | .map((q, i, qs) => i !== qs.length - 1 ? `${q}$${i}` : q) 103 | .join("").split(" ") 104 | 105 | let identifiers = 106 | bt.isTemplateLiteral(as[0]) ? as[0].expressions : [] 107 | 108 | if (as[1] && !bt.isExpression(as[1])) throw "Expected an expression, at first argument" 109 | let cnd = as[1] 110 | 111 | let os = (cnd !== undefined ? osCor.slice(0, -1) : osCor) as Operator[] 112 | let cor = (cnd ? osCor.slice(-1)[0] : undefined) as Comparator | undefined 113 | 114 | for (let o of os) { 115 | if (isIndex(o)) continue 116 | if (isTypeof(o)) continue 117 | if (isBitwiseAnd(o)) continue 118 | assertNever(o, `Unexpected operator "${o}"`) 119 | } 120 | if (cnd) { 121 | if (!cor) throw `Expected an comparator` 122 | if (!isComparator(cor)) throw `Unexpected comparator "${cor}"` 123 | } 124 | 125 | if (!cnd) return { value: os, identifiers } as PArguments 126 | return { value: [...os, cor, cnd], identifiers } as PArguments 127 | 128 | }, e => `${e}, at ${loc(node)}`) 129 | 130 | const isIndex = (o: string): o is `${`?` | `.`}${string}` => 131 | o.startsWith(".") || o.startsWith("?.") 132 | 133 | const isTypeof = (o: string): o is "typeof" => 134 | o === "typeof" 135 | 136 | const isBitwiseAnd = (o: string): o is `&${string}` => 137 | o.startsWith("&") 138 | 139 | const isComparator = (c: string): c is "===" | "!==" => 140 | c === "===" || c === "!==" 141 | 142 | const isComparand = (n: bt.Node): n is Comparand => 143 | bt.isExpression(n) 144 | 145 | 146 | 147 | 148 | // ---------- 149 | // pa 150 | 151 | const transformPa = (refs: btNodePath[]) => { 152 | for (let path of refs.map(r => r.parentPath)) 153 | path?.replaceWith(paMacro(path.node)) 154 | } 155 | 156 | const paMacro = (node: bt.Node) => doAndMapStringError(() => { 157 | if (!bt.isCallExpression(node)) throw "`pa` was expected to be called" 158 | 159 | let as = node.arguments 160 | if (as.length !== 2) throw "Expected 2 arguments" 161 | 162 | let [a0, a1] = as as [bt.Node, bt.Node] 163 | if (!bt.isExpression(a0)) throw "Expected an expression, at first argument" 164 | if (!bt.isExpression(a1)) throw "Expected an expression, at second argument" 165 | 166 | return bt.callExpression(a1, [a0]) 167 | 168 | }, e => `${e}, at ${loc(node)}`) 169 | 170 | 171 | 172 | 173 | // ---------- 174 | // pt 175 | 176 | const transformPm = (refs: btNodePath[]) => { 177 | for (let path of refs.map(r => r.parentPath)) 178 | path?.replaceWith(pmMacro(path.node)) 179 | } 180 | 181 | const pmMacro = (node: bt.Node) => doAndMapStringError(() => { 182 | if (!bt.isCallExpression(node)) throw "`pm` was expected to be called" 183 | 184 | let as = node.arguments 185 | if (as.length !== 1) throw "Expected 1 argument" 186 | 187 | let [a0] = as as [bt.Node] 188 | if (!bt.isExpression(a0)) throw "Expected an expression, at first argument" 189 | 190 | return bt.arrowFunctionExpression( 191 | [bt.identifier("t")], 192 | bt.binaryExpression( 193 | "===", 194 | bt.memberExpression( 195 | bt.callExpression(a0, [bt.identifier("t")]), 196 | bt.identifier("length") 197 | ), 198 | bt.numericLiteral(1) 199 | ) 200 | ) 201 | }, e => `${e}, at ${loc(node)}`) 202 | 203 | 204 | // ---------- 205 | // extras 206 | 207 | const parseExpression = (code: string) => { 208 | let file = parse(code) as bt.File 209 | return (file.program.body[0]! as bt.ExpressionStatement).expression 210 | } 211 | 212 | const doAndMapStringError = (e: () => R, f: (e: string) => unknown) => { 213 | try { return e() } 214 | catch (e) { 215 | if (typeof e !== "string") throw e 216 | throw f(e) 217 | } 218 | } 219 | 220 | const loc = (node: bt.Node) => { 221 | if (!node) return ":" 222 | let start = node.loc?.start 223 | if (!start) return ":" 224 | return start.line + ":" + start.column; 225 | } 226 | 227 | const assertNever: (a?: never, error?: unknown) => never = (a, error) => { 228 | throw (error ?? new Error( 229 | "Invariant: `assertNever` called with " + 230 | JSON.stringify(a, null, " ") 231 | )); 232 | } 233 | 234 | return main() 235 | }) 236 | -------------------------------------------------------------------------------- /packages/predicate/src/types.ts: -------------------------------------------------------------------------------- 1 | export { P, Pa, Pm } 2 | 3 | import * as N from "@sthir/number" 4 | 5 | // ---------- 6 | // P 7 | 8 | type P = 9 | > 10 | (...a: PArgsNarrowed) => 11 | (t: T) => t is 12 | ( A extends [] ? ["!==", A.Falsy] : 13 | A extends [infer Os] ? [`${Os & string} !==`, A.Falsy] : 14 | A 15 | ) extends [infer OsCor, infer Cnd] 16 | ? S.Split extends [...infer Os, infer Cor] 17 | ? I.Intersect< 18 | T, 19 | Constraint, Cor, Cnd> 20 | > 21 | : never 22 | : never 23 | 24 | type PArgs = 25 | [...( 26 | Self extends [`${Operator} ${string}`, unknown?] 27 | ? Self extends [`${infer O} ${infer R}`, unknown?] 28 | ? PArgsR, [R]>> 29 | : never : 30 | Self extends [Comparator, unknown?] 31 | ? Self extends [infer C, unknown?] 32 | ? [C & string, Comparand] 33 | : never : 34 | | [] 35 | | [Operator] 36 | | [Comparator] 37 | )] 38 | 39 | type PArgsNarrowed = 40 | { [I in keyof T]: 41 | [I, T] extends [1, [`${string} &${number}`, number]] ? number : T[I] 42 | } 43 | 44 | type PArgsR = 45 | T extends unknown 46 | ? T["length"] extends 0 ? [`${O}`] : 47 | T["length"] extends 1 ? [`${O} ${T[0] & string}`] : 48 | T["length"] extends 2 ? [`${O} ${T[0] & string}`, T[1]] : 49 | never 50 | : never 51 | 52 | type Operator = 53 | | IndexFromPath> 54 | | "typeof" 55 | | (T extends number ? `&${number}` : never) 56 | 57 | type Operate = 58 | T extends unknown 59 | ? O extends `${"?" | ""}.${string}` 60 | ? A.Get> : 61 | O extends "typeof" 62 | ? T extends string ? "string" : 63 | T extends number ? "number" : 64 | T extends bigint ? "bigint" : 65 | T extends boolean ? "boolean" : 66 | T extends symbol ? "symbol" : 67 | T extends undefined ? "undefined" : 68 | T extends null ? "object" : 69 | T extends object ? "object" : 70 | never : 71 | O extends `&${infer X}` 72 | ? N.E<`${T & number} & ${X}`> : 73 | never 74 | : never 75 | 76 | type NormaliseOperations = 77 | Os extends [] ? [] : 78 | Os extends [infer O] 79 | ? O extends `${"?" | ""}.${string}` 80 | ? [...S.SplitBefore] 81 | : [O] : 82 | Os extends [infer Oh, ...infer Ot] 83 | ? [...NormaliseOperations<[Oh]>, ...NormaliseOperations] : 84 | never 85 | 86 | type Comparator = 87 | | "===" 88 | | "!==" 89 | 90 | type Comparand = 91 | T extends unknown 92 | ? C extends "===" ? T : 93 | C extends "!==" ? T : 94 | never 95 | : never 96 | 97 | type Constraint = 98 | Cor extends "===" 99 | ? Os extends [] ? Cnd : 100 | Os extends [infer Oh, ...infer Ot] 101 | ? Oh extends `${"?" | ""}.${string}` 102 | ? A.Pattern< 103 | PathFromIndex, 104 | Ot extends [] ? Cnd : Constraint 105 | > : 106 | Ot extends [] 107 | ? I.Operator 108 | : unknown 109 | : never : 110 | Cor extends "!==" 111 | ? I.Not> : 112 | never 113 | 114 | 115 | namespace I { 116 | declare const $$not: unique symbol 117 | export type Not = { [$$not]: T } 118 | 119 | declare const $$operator: unique symbol 120 | export type Operator = { [$$operator]: [T, R] } 121 | 122 | export type Intersect ? B : _B, 124 | IsNot = B extends _B ? false : true, 125 | ACopy = A 126 | > = 127 | [B] extends [I.Operator] 128 | ? A extends unknown 129 | ? Operate extends R 130 | ? IsNot extends true ? never : A 131 | : IsNot extends true ? A : never 132 | : never : 133 | A extends object 134 | ? A.Get : never, 144 | IsNot extends true ? I.Not : B[K] 145 | > 146 | }> 147 | > 148 | : A 149 | ] 150 | : never 151 | >, 0> 152 | : IsNot extends true 153 | ? A extends B ? never : A 154 | : A & B 155 | 156 | type Test0 = A.Test>, 158 | "C" 159 | >> 160 | 161 | type Test1 = A.Test>, 163 | "C" 164 | >> 165 | 166 | type Test2 = A.Test>, 168 | | ({ a: string | undefined } & { a: string }) 169 | | ({ b: string } & { a: string }) 170 | >> 171 | 172 | type Test3 = A.Test 176 | >, 177 | | ({ y: "X" | "Z" } & { x: undefined } & { y: "Z" | undefined }) 178 | | "C" 179 | >> 180 | 181 | type Test4 = A.Test>, 183 | | ({ x: "A" | "Z" } & { x: "Z" | undefined }) 184 | | "C" 185 | >> 186 | 187 | type Test5 = A.Test 200 | >, 201 | | ( { x: "A" 202 | , y: { x: "B" | "Z" } 203 | } 204 | & { y: 205 | | ( { x: "B" | "Z" } 206 | & { x: "Z" | undefined } 207 | ) 208 | | undefined; 209 | } 210 | & { z: undefined } 211 | ) 212 | | "C" 213 | >> 214 | 215 | type Test6 = A.Test>, 217 | string 218 | >> 219 | 220 | type Test7 = A.Test>, 222 | { a?: string } & { a: string } 223 | >> 224 | 225 | type Test8 = A.Test>>, never>> 226 | type Test9 = A.Test>>, 0b101>> 227 | type Test10 = A.Test }>>, "foo">> 228 | } 229 | 230 | // ---------- 231 | // Pa 232 | 233 | type Pa = 234 | 235 | (t: T, p: (t: T) => t is U) => 236 | t is U 237 | 238 | 239 | // ---------- 240 | // Pm 241 | 242 | type Pm = 243 | 244 | (f: (t: T) => [U] | []) => 245 | (t: T) => t is U 246 | 247 | 248 | // ---------- 249 | // extras 250 | 251 | type IndexFromPath

= 252 | U.Exclude}`, ".?", "?.">, "."> 253 | 254 | type PathFromIndex = 255 | S.Split< 256 | S.Replace< 257 | S.ReplaceLeading, "?.", "?">, 258 | "?.", "." 259 | >, 260 | "." 261 | > 262 | 263 | namespace L { 264 | export type Join = 265 | L extends [] ? "" : 266 | L extends [infer Lh] ? A.Cast : 267 | L extends [infer Lh, ...infer Lt] 268 | ? `${A.Cast}${A.Cast}${L.Join}` : 269 | never 270 | 271 | export type Shifted = 272 | L extends [] ? [] : 273 | L extends [unknown, ...infer T] ? T : 274 | never 275 | 276 | export type Reverse = 277 | L extends [] ? [] : 278 | L extends [infer H, ...infer T] ? [...Reverse, H] : 279 | never 280 | } 281 | 282 | namespace S { 283 | export type Split = 284 | S extends "" ? [] : 285 | S extends `${infer Sh}${A.Cast}${infer St}` 286 | ? [Sh, ...Split] : 287 | [S] 288 | 289 | export type SplitBefore = 290 | S extends "" ? [] : 291 | S extends `${infer Sh}${A.Cast}${infer St}` 292 | ? S extends `${Sh}${infer Dh}${St}` 293 | ? [ ...(Sh extends "" ? [] : [Sh]) 294 | , ...( 295 | Split extends infer X 296 | ? X extends [] ? [] : 297 | X extends [infer H, ...infer T] 298 | ? [`${Dh}${A.Cast}`, ...T] 299 | : never 300 | : never 301 | ) 302 | ] 303 | : never : 304 | [S] 305 | 306 | type Test0 = A.Test, ["a", "?.b"]>> 307 | type Test1 = A.Test, ["?.b"]>> 308 | 309 | export type Replace = 310 | S extends X ? W : 311 | S extends `${A.Cast}${infer T}` 312 | ? L.Join<[W, Replace], ""> : 313 | S extends `${infer H}${infer T}` 314 | ? L.Join<[H, Replace], ""> : 315 | S 316 | 317 | export type ReplaceLeading = 318 | S extends X ? W : 319 | S extends (X extends unknown ? `${A.Cast}${infer T}` : never) 320 | ? `${A.Cast}${T}` : 321 | S 322 | } 323 | 324 | namespace A { 325 | export type Path = _Path 326 | export type _Path = 327 | T extends unknown ? 328 | T extends Visited ? [] : 329 | T extends A.Primitive ? [] : 330 | keyof T extends never ? [] : 331 | | [] 332 | | ( keyof T extends infer K 333 | ? K extends unknown 334 | ? _Path : never, Visited | T> extends infer P 335 | ? P extends unknown 336 | ? [ K extends keyof T_ ? K : 337 | [T_] extends [{}] ? K : 338 | `?${A.Cast}` 339 | , ...A.Cast 340 | ] 341 | : never 342 | : never 343 | : never 344 | : never 345 | ) 346 | : never 347 | 348 | type Test0 = A.Test, 353 | [] | ["a"] | ["c"] | ["c", "x"] | ["c", "y"] | ["c", "z"] 354 | >> 355 | 356 | type Test1 = A.Test, 362 | [] | ["a"] | ["b"] | ["c"] | ["c", "?x"] | ["c", "?y"] | ["c", "?z"] 363 | >> 364 | 365 | type Test2 = A.Test, 367 | [] | ["?a"] 368 | >> 369 | 370 | type Test3 = A.Test, 372 | [] | ["a"] 373 | >> 374 | 375 | interface Foo { foo: Foo } 376 | type Test4 = A.Test, 378 | [] | ["foo"] 379 | >> 380 | 381 | 382 | export type Pattern = 383 | P extends [] ? V : 384 | P extends [infer Ph, ...infer Pt] 385 | ? Ph extends `?${infer K}` 386 | ? { [_ in K]: Pattern } | null | undefined 387 | : { [_ in A.Cast]: Pattern } : 388 | never 389 | 390 | export type Falsy = 391 | false | undefined | null | 0 | 0n | "" 392 | 393 | export type Get = 394 | B.Not> extends true ? Get : 395 | P extends [] ? T : 396 | P extends [infer Ph] ? 397 | (Ph extends `?${infer X}` ? X : Ph) extends infer K 398 | ? K extends keyof T ? T[K] : 399 | T extends null ? null : 400 | T extends undefined ? undefined : 401 | undefined 402 | : never : 403 | P extends [infer Ph, ...infer Pr] ? Get, Pr> : 404 | never 405 | 406 | export type InferStringLiteralTuple = 407 | T extends string[] ? T : string[] 408 | 409 | export type DoesExtend = 410 | A extends B ? true : false 411 | 412 | export type Cast = 413 | T extends U ? T : U 414 | 415 | export type Templateable = 416 | | string 417 | | number 418 | | boolean 419 | | null 420 | | undefined 421 | | bigint 422 | 423 | export type Primitive = 424 | | string 425 | | number 426 | | boolean 427 | | null 428 | | undefined 429 | | bigint 430 | | symbol 431 | 432 | export type AreEqual = 433 | (() => T extends B ? 1 : 0) extends (() => T extends A ? 1 : 0) 434 | ? true 435 | : false; 436 | 437 | export type Test = T 438 | } 439 | 440 | namespace U { 441 | export type Exclude = 442 | T extends U ? never : T 443 | 444 | export type ToIntersection = 445 | (T extends unknown ? (_: T) => void : never) extends ((_: infer I) => void) 446 | ? I 447 | : never; 448 | } 449 | 450 | namespace B { 451 | export type Not = 452 | T extends true ? false : true 453 | } 454 | 455 | namespace O { 456 | export type Normalize = 457 | {} extends T ? unknown : 458 | unknown extends (K extends unknown ? A.Get extends never ? unknown : never : never) 459 | ? never : 460 | T 461 | 462 | type Test1 = A.Test, never>> 463 | } -------------------------------------------------------------------------------- /packages/predicate/tests/.gitignore: -------------------------------------------------------------------------------- 1 | /types.test.ts -------------------------------------------------------------------------------- /packages/predicate/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { p, pa, pm } from "../src" 2 | 3 | test("implementation", () => { 4 | expect(pa( 5 | { a: 1 } as { a: { b: number } | number } | number, 6 | p(".a ?.b typeof ===", "undefined") 7 | )).toBe(true) 8 | 9 | expect(pa( 10 | 0b11 as number, 11 | p(`&${0b10}`) 12 | )).toBe(0b11 & 0b10) 13 | 14 | expect(pa( 15 | "test" as string | number, 16 | pm(x => typeof x === "string" ? [x] : []) 17 | )).toBe(true) 18 | }) -------------------------------------------------------------------------------- /packages/predicate/tests/jest-expect-extras.ts: -------------------------------------------------------------------------------- 1 | expect.extend({ 2 | toSetEqual(received, expected) { 3 | if (!( 4 | Array.isArray(received) && 5 | received.every(t => typeof t === "string" || typeof t === "number") 6 | )) { 7 | return { pass: false, message: () => `expected an array of strings or numbers only` } 8 | } 9 | 10 | if (!this.equals(received.slice().sort(), expected.slice().sort())) { 11 | return { 12 | pass: false, 13 | message: 14 | () => `expected both sets to be equal\n\nExpected: ${JSON.stringify(expected)}\nReceived: ${JSON.stringify(received)}` 15 | } 16 | } 17 | 18 | return { pass: true, message: () => "" } 19 | } 20 | }) 21 | 22 | declare global { 23 | namespace jest { 24 | interface Matchers { 25 | toSetEqual(b: string[]): R; 26 | } 27 | } 28 | } 29 | 30 | export {} -------------------------------------------------------------------------------- /packages/predicate/tests/macro/index.test.ts: -------------------------------------------------------------------------------- 1 | import tester from "babel-plugin-tester" 2 | import macros from "babel-plugin-macros" 3 | 4 | tester({ 5 | plugin: macros, 6 | formatResult: a => a, 7 | pluginName: "@sthir/predicate/macro", 8 | tests: { 9 | "index, typeof, ===": { 10 | code: ` 11 | import { p, pa } from "@sthir/predicate/macro"; 12 | 13 | pa(x, p(".a?.b typeof ===", y)); 14 | `, 15 | output: ` 16 | (t => typeof t.a?.b === y)(x); 17 | ` 18 | }, 19 | "no operators": { 20 | code: ` 21 | import { p, pa } from "@sthir/predicate/macro"; 22 | 23 | pa(x, p("===", y)); 24 | `, 25 | output: ` 26 | (t => t === y)(x); 27 | ` 28 | }, 29 | "truthy": { 30 | code: ` 31 | import { p, pa } from "@sthir/predicate/macro"; 32 | 33 | pa(x, p()); 34 | `, 35 | output: ` 36 | Boolean(x); 37 | ` 38 | }, 39 | "&": { 40 | code: ` 41 | import { p, pa } from "@sthir/predicate/macro"; 42 | 43 | pa(x, p(\`typeof &\${(() => y)()} ===\`, z)); 44 | `, 45 | output: ` 46 | (t => (typeof t & (() => y)()) === z)(x); 47 | ` 48 | }, 49 | "pt": { 50 | code: ` 51 | import { pa, pm } from "@sthir/predicate/macro"; 52 | 53 | pa(x, pm(x => typeof x === "string" ? [x] : [])) 54 | `, 55 | output: ` 56 | (t => (x => typeof x === "string" ? [x] : [])(t).length === 1)(x); 57 | ` 58 | } 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /packages/predicate/tests/types.twoslash-test.ts: -------------------------------------------------------------------------------- 1 | import { p, pa } from "../src" 2 | import * as N from "@sthir/number" 3 | import "./jest-expect-extras" 4 | 5 | const query = () => 6 | ((global as any).twoSlashQueries.shift()) as { completions: string[], text: string } 7 | 8 | test("index index typeof === value", () => { 9 | let x = {} as { a: { b: number }, z: string } | { c: string } | string 10 | 11 | pa(x, p()) 12 | 13 | // @ts-expect-error 14 | pa(x, p(" ")) 15 | // ^| 16 | expect(query().completions).toSetEqual( 17 | [".a", ".a?.b", ".c", ".z", "typeof", "===", "!=="] 18 | ) 19 | 20 | pa(x, p(".a")) 21 | 22 | // @ts-expect-error 23 | pa(x, p(".a ")) 24 | // ^| 25 | expect(query().completions).toSetEqual( 26 | [".a ?.b", ".a typeof", ".a ===", ".a !=="] 27 | ) 28 | 29 | pa(x, p(".a ?.b")) 30 | 31 | // @ts-expect-error 32 | pa(x, p(".a ?.b ")) 33 | // ^| 34 | expect(query().completions).toSetEqual( 35 | [".a ?.b typeof", ".a ?.b ===", ".a ?.b !=="] 36 | ) 37 | 38 | pa(x, p(".a ?.b typeof")) 39 | 40 | // @ts-expect-error 41 | pa(x, p(".a ?.b typeof ")) 42 | // ^| 43 | expect(query().completions).toSetEqual( 44 | [".a ?.b typeof typeof", ".a ?.b typeof ===", ".a ?.b typeof !=="] 45 | ) 46 | 47 | // @ts-expect-error 48 | pa(x, p(".a ?.b typeof ===")) 49 | 50 | // @ts-expect-error 51 | pa(x, p(".a ?.b typeof ===", " ")) 52 | // ^| 53 | expect(query().completions).toSetEqual(["number", "undefined"]) 54 | 55 | // @ts-expect-error 56 | x.z 57 | 58 | if (pa(x, p(".a ?.b typeof ===", "number"))) { 59 | // @ts-ignore TODO: breaks in TS 5.8.2, works in TS 4.8.0-beta 60 | expectAreTypesEqual().toBe(true) 61 | } 62 | }) 63 | 64 | test("typeof typeof ===", () => { 65 | let x = { lol: 0 } 66 | 67 | if (pa(x, p("typeof typeof ===", "string"))) { 68 | expectAreTypesEqual().toBe(true) 69 | } 70 | }) 71 | 72 | 73 | test("index typeof !==", () => { 74 | let x = {} as { a: string | number | undefined } | { b: string } 75 | 76 | if (pa(x, p(".a typeof !==", "undefined"))) { 77 | expectAreTypesEqual().toBe(true) 78 | } 79 | }) 80 | 81 | test("truthy", () => { 82 | let x = {} as { a: string } | number | undefined 83 | 84 | if (pa(x, p())) { 85 | expectAreTypesEqual().toBe(true) 86 | } 87 | }) 88 | 89 | test("index, truthy", () => { 90 | let x = {} as { a?: string } 91 | 92 | if (pa(x, p(".a"))) { 93 | expectAreTypesEqual().toBe(true) 94 | } 95 | }) 96 | 97 | test("no operators", () => { 98 | ;[1, 2, null].filter(p()) 99 | 100 | // @ts-expect-error 101 | ;[1, 2, null].filter(p(" ")) 102 | // ^| 103 | expect(query().completions).toSetEqual(["===", "!==", "typeof"]) 104 | 105 | // @ts-expect-error 106 | ;[1, 2, null].filter(p("!==")) 107 | 108 | let x = [1, 2, null].filter(p("!==", null)); 109 | 110 | expectAreTypesEqual().toBe(true) 111 | }) 112 | 113 | test("Issue #1", () => { 114 | let x = {} as { a: string } | undefined 115 | 116 | if (pa(x, p("?.a"))) { 117 | expectAreTypesEqual().toBe(true) 118 | } 119 | }) 120 | 121 | test("Issue #2", () => { 122 | let x = {} as { a: { b: string } | undefined } 123 | 124 | if (pa(x, p(".a?.b"))) { 125 | expectAreTypesEqual().toBe(true) 126 | } 127 | }) 128 | 129 | test("&", () => { 130 | interface A { flags: 0b101, foo: string } 131 | interface B { flags: 0b110, foo: number } 132 | interface C { flags: 0b010, foo: boolean } 133 | let x = {} as A | B | C 134 | 135 | if (pa(x, p(`.flags &${0b100}`))) { 136 | expectAreTypesEqual().toBe(true) 137 | } 138 | 139 | if (pa(x, p(`.flags &${0b100} !==`, 0))) { 140 | expectAreTypesEqual().toBe(true) 141 | } 142 | 143 | if (pa(x, p(`.flags &${0b100} ===`, 0b100))) { 144 | expectAreTypesEqual().toBe(true) 145 | } 146 | 147 | if (pa(x, p(`.flags &${0b111} ===`, 0b101))) { 148 | expectAreTypesEqual().toBe(true) 149 | } 150 | 151 | if (pa(x, p(`.flags &${Math.random() > 0.5 ? 0b100 : 0b101} !==`, 0))) { 152 | expectAreTypesEqual().toBe(true) 153 | } 154 | }) 155 | 156 | test("& for Jason's tweet 1471212197183651841", () => { 157 | // https://twitter.com/_developit/status/1471212197183651841 158 | 159 | const Flag = { 160 | Text: N.e("1 << 0"), 161 | Element: N.e("1 << 1"), 162 | Component: N.e("1 << 2"), 163 | A: N.e("1 << 3"), 164 | B: N.e("1 << 4"), 165 | C: N.e("1 << 5"), 166 | } 167 | type Flag = typeof Flag 168 | type NonNodeFlag = Flag["A"] | Flag["B"] | Flag["C"]; 169 | 170 | type PreactNode = 171 | TextNode | ElementNode | ComponentNode; 172 | 173 | interface TextNode 174 | { flags: N.E<`${Flag["Text"]} | ${NonNodeFlag}`> 175 | , text: string 176 | } 177 | 178 | interface ElementNode 179 | { flags: N.E<`${Flag["Element"]} | ${NonNodeFlag}`> 180 | , element: unknown 181 | } 182 | 183 | interface ComponentNode 184 | { flags: N.E<`${Flag["Component"]} | ${NonNodeFlag}`> 185 | , component: unknown 186 | } 187 | 188 | const render = (node: PreactNode) => { 189 | if (node.flags & Flag.Text) { 190 | // @ts-expect-error 191 | node.text 192 | } 193 | 194 | if (pa(node, p(`.flags &${Flag.Text}`))) { 195 | expectAreTypesEqual().toBe(true) 196 | } 197 | } 198 | }) 199 | 200 | test("Issue #8: index recursive", () => { 201 | interface Foo { foo: Foo } 202 | let x = {} as Foo 203 | 204 | pa(x, p()) 205 | 206 | pa(x, p(".foo")) 207 | 208 | pa(x, p(".foo .foo")) 209 | }) 210 | 211 | test("Issue #11: Normalize an object to never if value at some key is never", () => { 212 | let x = {} as { a: number; b: string } | { a: string; b: number } 213 | if (pa(x, p(".a typeof ===", "number"))) { 214 | expectAreTypesEqual().toBe(true) 215 | } 216 | }) 217 | 218 | const expectAreTypesEqual = 219 | () => ({ 220 | toBe: 221 | ( _: 222 | (() => T extends B ? 1 : 0) extends (() => T extends A ? 1 : 0) 223 | ? true 224 | : false 225 | ) => {} 226 | }) 227 | -------------------------------------------------------------------------------- /packages/runtime-checker/README.md: -------------------------------------------------------------------------------- 1 | See [this thread](https://x.com/devanshj__/status/1637827362539687937) for an introduction. 2 | -------------------------------------------------------------------------------- /packages/runtime-checker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sthir/runtime-checker", 3 | "version": "0.8.0", 4 | "description": "Parsers to perform runtime type checking", 5 | "main": "dist/sthir-runtime-checker.cjs.js", 6 | "module": "dist/sthir-runtime-checker.esm.js", 7 | "sideEffects": false, 8 | "preconstruct": { "entrypoints": ["index.ts"] }, 9 | "dependencies": {}, 10 | "devDependencies": {}, 11 | "author": { 12 | "name": "Devansh Jethmalani", 13 | "email": "jethmalani.devansh@gmail.com" 14 | }, 15 | "license": "MIT", 16 | "readme": "https://github.com/devanshj/sthir/tree/main/packages/runtime-checker/README.md", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/devanshj/sthir.git" 20 | }, 21 | "scripts": { 22 | "test": "jest", 23 | "build": "cd ../../ && preconstruct build" 24 | }, 25 | "jest": { 26 | "preset": "ts-jest", 27 | "testEnvironment": "node", 28 | "prettierPath": "", 29 | "snapshotFormat": { 30 | "printBasicPrototype": false 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /packages/runtime-checker/src/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: 2 | // - rewrite `accumulateErrors` 3 | // - (maybe) cachify iterables 4 | // - (maybe) functionalify some parts around iterables 5 | // - tests 6 | // - ok and innerOk with score 7 | 8 | export { 9 | Parser, 10 | 11 | name_ as name, 12 | N, 13 | is, 14 | assert, 15 | 16 | intersect, 17 | union, 18 | then, 19 | 20 | object, 21 | record, 22 | 23 | tuple, 24 | array, 25 | 26 | string, 27 | number, 28 | boolean, 29 | null_ as null, 30 | undefined_ as undefined, 31 | symbol, 32 | bigint, 33 | 34 | value, 35 | predicate, 36 | 37 | bindLazy, 38 | UnknownParser, 39 | Parsed, 40 | Parsee, 41 | } 42 | 43 | 44 | // ---------------------------------------------------------------------------------------------------- 45 | 46 | /** 47 | * parses a value of type `A` into type `T` 48 | * @param T parsed type 49 | * @param A parsee type (default is `unknown`) 50 | */ 51 | interface Parser 52 | { (this: unknown, a: A): Generator, void> 53 | , typeName?: string 54 | } 55 | 56 | type ParserThis = 57 | | undefined 58 | | { typeName?: string } 59 | 60 | /** supertype of all parsers, ie all parsers are assignable to this type */ 61 | interface UnknownParser 62 | extends Parser {} 63 | 64 | type ParserYield = 65 | | void 66 | | ParserOkYield 67 | | ParserErrorYield 68 | | ParserInnerOkYield 69 | 70 | interface ParserOkYield 71 | { type: "ok", value: T } 72 | 73 | interface ParserErrorYield 74 | { type: "error", value: string } 75 | 76 | interface ParserInnerOkYield 77 | { type: "innerOk" } 78 | 79 | type Parsed

= 80 | P extends Parser ? T : never 81 | 82 | type ParsedWrapped

= 83 | P extends Parser ? [T] : never 84 | 85 | type Parsee

= 86 | P extends Parser ? A : never 87 | 88 | type ParseeWrapped

= 89 | P extends Parser ? [A] : never 90 | 91 | interface _T 92 | { _isT: true } 93 | 94 | interface _U 95 | { _isU: true } 96 | 97 | interface _A 98 | { _isA: true } 99 | 100 | type _K = 101 | & PropertyKey 102 | & { _isK: true } 103 | 104 | 105 | // ---------------------------------------------------------------------------------------------------- 106 | 107 | type IntersectR = 108 | { 109 | (contituents: [...Ps]): 110 | Intersect 111 | , > 112 | (contituents: Ps): 113 | IntersectIterable 114 | } 115 | 116 | type Intersect = 117 | { [I in keyof Ps]: [Parsed] }[number] extends infer T ? 118 | { [I in keyof Ps]: [Parsee] }[number] extends infer A ? 119 | Parser, WrappedUnionToIntersection> : never : never 120 | 121 | type IntersectIterable> = 122 | Ps extends Iterable 123 | ? ParserAsserted< 124 | WrappedUnionToIntersection>, 125 | WrappedUnionToIntersection> 126 | > 127 | : never 128 | 129 | type WrappedUnionToIntersection = 130 | (U extends unknown ? ((u: U) => void) : never) extends ((i: infer I) => void) ? I[0 & keyof I] : never 131 | 132 | type TestL98 = 133 | Test, Parser<{ c: "c" }>]>, 135 | Parser<({ a: "a" } | { b: "b" }) & { c: "c" }> 136 | >> 137 | 138 | type TestL105 = 139 | Test, 142 | Parser<{ c: "c" }, { c: string | number }> 143 | ]>, 144 | Parser< 145 | ({ a: "a" } | { b: "b" }) & { c: "c" }, 146 | { a?: string, b?: string } & { c: string | number } 147 | > 148 | >> 149 | 150 | type IntersectImpl = 151 | (ps: Iterable>) => Parser<_T, unknown> 152 | 153 | let intersectImpl: IntersectImpl = ps => { 154 | if (Array.isArray(ps)) { 155 | ps = ps.filter(p => p !== unknown) 156 | if ((ps as Parser<_T>[]).length === 1) return (ps as Parser<_T>[])[0]! 157 | } 158 | return function*(a) { 159 | let gs = [] as ReturnType>[] 160 | let psLength = Infinity 161 | let irs = [] as number[] 162 | 163 | let everyOk = true 164 | while (psLength > irs.length) { 165 | let i = -1 166 | for (let p of ps) { i++ 167 | if (irs.includes(i)) continue 168 | let g = gs[i] ?? (gs[i] = p(a)) 169 | let y = g.next().value 170 | if (!y) { irs.push(i); continue } 171 | if (y.type === "error") { yield { type: "error", value: y.value }; everyOk = false; continue } 172 | if (y.type === "innerOk") { yield y; continue } 173 | if (y.type === "ok") { yield { type: "innerOk" }; continue } 174 | assertNever(y) 175 | } 176 | psLength = i + 1 177 | } 178 | if (everyOk) yield { type: "ok", value: a as _T } 179 | } 180 | } 181 | 182 | const intersect = intersectImpl as unknown as IntersectR 183 | 184 | 185 | 186 | // ---------------------------------------------------------------------------------------------------- 187 | 188 | type UnionR = 189 | { 190 | (constituents: [...Ps], options?: UnionOptions): 191 | Union 192 | , > 193 | (constituents: Ps, options?: UnionOptions): 194 | UnionIterable 195 | , defaultOptions: Required 196 | } 197 | 198 | type UnionOptions = 199 | { maxErrorTries?: number 200 | } 201 | 202 | type Union = 203 | WrappedUnionToIntersection<{ [I in keyof Ps]: [Parsee] }[number]> extends infer A 204 | ? ParserAsserted<{ [I in keyof Ps]: Parsed }[number], A> 205 | : never 206 | 207 | type UnionIterable> = 208 | Ps extends Iterable 209 | ? ParserAsserted< 210 | WrappedUnionToIntersection>, 211 | WrappedUnionToIntersection> 212 | > 213 | : never 214 | 215 | type ParserAsserted = 216 | [T] extends [A] ? Parser : Parser 217 | 218 | type Test165 = 219 | Test, Parser<{ c: "c" }>]>, 221 | Parser<({ a: "a" } & { b: "b" }) | { c: "c" }> 222 | >> 223 | 224 | type TestL171 = 225 | Test, 228 | Parser<{ c: "c" }, { c: string | number }> 229 | ]>, 230 | Parser< 231 | (({ a: "a" } & { b: "b" }) | { c: "c" }) & ({ a?: string, b?: string } & { c: string | number }), 232 | { a?: string, b?: string } & { c: string | number } 233 | > 234 | >> 235 | 236 | type UnionImpl = 237 | (ps: Iterable>, options?: UnionOptions) => Parser<_T> 238 | 239 | let unionImpl: UnionImpl = (ps, _options) => function*(a) { 240 | let options = { ...union.defaultOptions, ..._options } 241 | let eP = `is not of type '${(this as ParserThis)?.typeName ?? ""}' as it ` 242 | let gs = [] as ReturnType>[] 243 | let bestI = undefined as number | undefined 244 | let someOk = false 245 | let es = [] as number[] 246 | let ss = [] as number[] 247 | let yss = [] as ParserYield<_T>[][] 248 | let irs = [] as number[] 249 | let psLength = Infinity 250 | 251 | root: while (psLength > irs.length) { 252 | let i = -1 253 | for (let p of ps) { i++ 254 | if (irs.includes(i)) continue; 255 | let g = gs[i] ?? (g => (gs[i] = g, ss[i] = 0, es[i] = 0, yss[i] = [], g))(p(a)) 256 | let y = g.next().value 257 | yss[i]!.push(y) 258 | if (!y) { irs.push(i); continue } 259 | if (y.type === "error") { 260 | es[i]!++ 261 | if (es[i]! >= options.maxErrorTries) irs.push(i); continue 262 | } 263 | if (y.type === "innerOk") { ss[i]!++; continue } 264 | if (y.type === "ok") { bestI = i; someOk = true; break root } 265 | assertNever(y) 266 | } 267 | psLength = i + 1 268 | } 269 | 270 | let bestP = undefined as Parser<_T> | undefined 271 | if (bestI === undefined) { 272 | let i = -1 273 | for (let p of ps) { i++ 274 | if (ss[i]! > (bestI !== undefined ? ss[bestI]! : -1)) { 275 | bestI = i 276 | bestP = p 277 | } 278 | } 279 | } 280 | let bestYs = yss[bestI!]! 281 | if (someOk) { 282 | yield* bestYs 283 | if (bestYs[bestYs.length - 1] !== undefined) { 284 | yield* gs[bestI!]! 285 | } 286 | } else { 287 | eP += `did not match any contituents, best match probably was '${bestP!.typeName ?? ""}' but` 288 | yield* bestYs.map((y): ParserYield<_T> => { 289 | if (y?.type === "error") { 290 | return { 291 | type: "error", 292 | value: eP + y.value.replace(/is not of type '[^']+' as/, "") 293 | } 294 | } 295 | return y 296 | }) 297 | if (bestYs.slice(-1)[0] !== undefined) { 298 | for (let y of gs[bestI!]!) { 299 | if (y?.type === "error") { 300 | yield { type: "error", value: eP + y.value.replace(/is not of type '[^']+' as/, "") }; 301 | continue 302 | } 303 | yield y 304 | } 305 | } 306 | } 307 | } 308 | const union = unionImpl as unknown as UnionR 309 | union.defaultOptions = { maxErrorTries: 5 } 310 | 311 | 312 | 313 | // ---------------------------------------------------------------------------------------------------- 314 | 315 | type ObjectR = 316 | > 317 | (ps: Ps) => 318 | ObjectT 319 | 320 | type ObjectT = 321 | ParserAsserted< 322 | UnifyStructure< 323 | & { [K in keyof Ps as ExtractNonOptionalProperty]: Parsed> } 324 | & { [K in keyof Ps as ExtractOptionalProperty]?: Parsed> } 325 | >, 326 | CleanStructure]: Parsee> } 328 | & { [K in keyof Ps as ExtractOptionalProperty]?: Parsee> } 329 | >> 330 | > 331 | 332 | type UnifyStructure = 333 | { [K in keyof T]: T[K] } & unknown 334 | 335 | type CleanStructure = 336 | IsStructureReducibleToUnknown extends true 337 | ? unknown 338 | : T extends unknown[] ? T : { [K in keyof T as unknown extends T[K] ? never : K]: T[K] } 339 | 340 | type IsStructureReducibleToUnknown = 341 | { [K in keyof T]-?: 342 | unknown extends T[K & keyof T] ? true : K 343 | }[keyof T & (T extends unknown[] ? number : unknown)] extends true ? true : false 344 | 345 | type ExtractOptionalProperty = 346 | K extends `${infer X}?` 347 | ? X extends `${string}\\` ? never : X 348 | : never 349 | 350 | type ExtractNonOptionalProperty = 351 | K extends `${infer X}?` 352 | ? X extends `${infer Y}\\` ? `${Y}?` : never 353 | : K 354 | 355 | type Cast = 356 | T extends U ? T : U 357 | 358 | type Test285 = 359 | Test, "b?": Parser<"b">, "c\\?": Parser<"c"> }>, 361 | Parser<{ a: "a", b?: "b", "c?": "c" }> 362 | >> 363 | 364 | type Test291 = 365 | Test, "b?": Parser<"b", string>, "c\\?": Parser<"c", string> }>, 367 | Parser< 368 | { a: "a", b?: "b", "c?": "c" }, 369 | { a: string, b?: string, "c?": string } 370 | > 371 | >> 372 | 373 | type Test300 = 374 | Test, "b?": Parser<"b", string>, "c\\?": Parser<"c"> }>, 376 | Parser< 377 | { a: "a", b?: "b", "c?": "c" }, 378 | { b?: string } 379 | > 380 | >> 381 | 382 | type ObjectImpl = 383 | (ps: Record>) => Parser> 384 | 385 | const objectImpl: ObjectImpl = ps => function*(a) { 386 | let eP = `is not of type '${(this as ParserThis)?.typeName ?? ""}' as it ` 387 | 388 | if (typeof a !== "object" || a === null) { 389 | yield { type: "error", value: eP + "is not an object" } 390 | return 391 | } 392 | let hasRequiredKeys = true 393 | for (let k in ps) { 394 | if (k.endsWith("?") && !k.endsWith("\\?")) continue 395 | if (!(k in a)) { 396 | hasRequiredKeys = false 397 | yield { type: "error", value: eP + `is missing key '${k.replace(/\\\?$/, "?")}'` } 398 | } 399 | } 400 | 401 | eP = eP.slice(0, "it ".length * -1) + "it's " 402 | for (let y of intersect({ 403 | [Symbol.iterator]: function*() { 404 | for (let k in a) { 405 | let p = ps[k] ?? ps[k + "?"] 406 | if (!p) continue 407 | yield function*(_: undefined) { 408 | let v = a[k as keyof typeof a] as _T 409 | for (let y of p!(v)) { 410 | if (y?.type === "error") { yield { type: "error" as "error", value: eP + `value at key '${k}' ${y.value}` }; continue } 411 | yield y 412 | } 413 | } 414 | } 415 | } 416 | })(undefined)) { 417 | if (y?.type === "ok") { 418 | if (hasRequiredKeys) yield { type: "ok", value: a as Record } 419 | continue 420 | } 421 | yield y 422 | } 423 | } 424 | 425 | const object = objectImpl as unknown as ObjectR 426 | 427 | // ---------------------------------------------------------------------------------------------------- 428 | 429 | type Tuple = 430 | 431 | (elements: ParseElementParsers) => 432 | TupleT 433 | 434 | type ParseElementParsers = 435 | Ps extends [] ? [] : 436 | Ps extends [infer X extends [unknown, "?"], ...infer R] 437 | ? [X, ...ParseElementParsers] : 438 | Ps extends [infer X, ...infer R] 439 | ? [ DidFindOptional extends true 440 | ? "Error: This element must be optional as it follows an optional element" 441 | : X 442 | , ...ParseElementParsers 443 | ] : 444 | never 445 | 446 | type TupleT = 447 | ParserAsserted, CleanStructure>> 448 | 449 | type TupleTParsed = 450 | Ps extends [] ? [] : 451 | Ps extends [[infer X extends UnknownParser, "?"], ...infer R] ? [Parsed?, ...TupleTParsed] : 452 | Ps extends [infer X extends UnknownParser, ...infer R] ? [Parsed, ...TupleTParsed] : 453 | never 454 | 455 | type TupleTParsee = 456 | Ps extends [] ? [] : 457 | Ps extends [[infer X extends UnknownParser, "?"], ...infer R] ? [Parsee?, ...TupleTParsee] : 458 | Ps extends [infer X extends UnknownParser, ...infer R] ? [Parsee, ...TupleTParsee] : 459 | never 460 | 461 | type Test373 = 462 | Test, Parser<"b">, [Parser<"c">, "?"]]>, 464 | Parser<["a", "b", "c"?]> 465 | >> 466 | 467 | type Test379 = 468 | Test, Parser<"b", string | number>, [Parser<"c", string>, "?"]]>, 470 | Parser<["a", "b", "c"?], [string, string | number, string?]> 471 | >> 472 | 473 | type Test397 = 474 | Test, Parser<"b", string | number>, [Parser<"c">, "?"]]>, 476 | Parser<["a", "b", "c"?], [unknown, string | number, unknown?]> 477 | // TODO: perhaps make the parsee `{ 1: string | number }`? Should the above parsee 478 | // have the same change? 479 | >> 480 | 481 | type TupleImpl = 482 | (ps: (Parser<_T> | [Parser<_T>, "?"])[]) => Parser> 483 | 484 | const tupleImpl: TupleImpl = ps => function*(a: unknown) { 485 | let eP: string = `is not of type '${(this as ParserThis)?.typeName ?? ""}' as it ` 486 | if (!Array.isArray(a)) { 487 | yield { type: "error", value: eP + "is not an array" } 488 | return 489 | } 490 | let minLength = ps.filter(p => !Array.isArray(p)).length 491 | let maxLength = ps.length 492 | if (a.length < minLength) { 493 | yield { type: "error", value: eP + "has missing indices" } 494 | return 495 | } 496 | if (a.length > maxLength) { 497 | yield { type: "error", value: eP + "has extra indices" } 498 | return 499 | } 500 | 501 | eP = eP.slice(0, "it ".length * -1) + "it's " 502 | for (let y of intersect({ 503 | [Symbol.iterator]: function*(){ 504 | for (let i of a) { 505 | let p = (x => Array.isArray(x) ? x[0] : x)(ps[i]) 506 | if (!p) continue 507 | yield function*(_: undefined) { 508 | let v = a[i as keyof typeof a] as _T 509 | for (let y of p!(v)) { 510 | if (y?.type === "error") { yield { type: "error" as "error", value: eP + `value at key '${i}' ${y.value}` }; continue } 511 | yield y 512 | } 513 | } 514 | } 515 | } 516 | })(undefined)) { 517 | if (!y) { yield y; continue } 518 | if (y.type === "ok") { yield { type: "ok", value: a as unknown[] & Record }; continue } 519 | yield y 520 | } 521 | } 522 | 523 | const tuple = tupleImpl as unknown as Tuple 524 | 525 | 526 | 527 | 528 | // ---------------------------------------------------------------------------------------------------- 529 | 530 | type RecordR = 531 | , V extends UnknownParser> 532 | (key: K, value: V) => 533 | RecordT 534 | 535 | type RecordT, V extends UnknownParser> = 536 | ParserAsserted< 537 | Record, Parsed>, 538 | [unknown, unknown] extends [Parsee, Parsee] 539 | ? unknown 540 | : Record & PropertyKey, Parsee> 541 | > 542 | 543 | type Test422 = 544 | Test, Parser<"b">>, 546 | Parser> 547 | >> 548 | 549 | type Test428 = 550 | Test, Parser<"b", string | number>>, 552 | Parser, Record> 553 | >> 554 | 555 | type RecordImpl = 556 | (k: Parser<_K>, v: Parser<_T>) => Parser> 557 | 558 | const recordImpl: RecordImpl = (k, v) => function*(a) { 559 | let eP = `is not of type '${(this as ParserThis)?.typeName ?? ""}' as it ` 560 | 561 | if (typeof a !== "object" || a === null) { 562 | yield { type: "error", value: eP + "is not an object" } 563 | return 564 | } 565 | 566 | eP = eP.slice(0, "it ".length * -1) + "it's " 567 | for (let y of intersect({ 568 | [Symbol.iterator]: function*() { 569 | for (let ak in a) { 570 | let av = a[ak as keyof typeof a] as _T 571 | yield intersect([ 572 | k === unknown || k === string ? unknown : function*(_: undefined) { 573 | for (let y of k(ak)) { 574 | if (y?.type === "error") { yield { type: "error", value: eP + `key '${ak}' ${y.value}` }; continue } 575 | yield y 576 | } 577 | }, 578 | function*(_: undefined) { 579 | for (let y of v(av)) { 580 | if (y?.type === "error") { yield { type: "error", value: eP + `value at key '${ak}' ${y.value}` }; continue } 581 | yield y 582 | } 583 | } 584 | ]) 585 | } 586 | } 587 | })(undefined)) { 588 | if (!y) { yield y; continue } 589 | if (y.type === "ok") { yield { type: "ok", value: a as Record<_K, _T> }; continue } 590 | yield y 591 | } 592 | } 593 | 594 | const record = recordImpl as unknown as RecordR 595 | 596 | 597 | 598 | // ---------------------------------------------------------------------------------------------------- 599 | 600 | type Array = 601 |

602 | (element: P) => 603 | ParserAsserted[], unknown extends Parsee

? unknown : Parsee

[]> 604 | 605 | type ArrayImpl = 606 | (p: Parser<_T>) => Parser> 607 | // adding `unknown[] & ` breaks in TS 5.7.2 but used to work in TS 4.8.0-beta 608 | 609 | const arrayImpl: ArrayImpl = p => then( 610 | function*(a: unknown) { 611 | let eP: string = `is not of type '${(this as ParserThis)?.typeName ?? ""}' as it ` 612 | if (!Array.isArray(a)) { 613 | yield { type: "error", value: eP + "is not an array" } 614 | return 615 | } 616 | yield { type: "ok", value: a as unknown[] }; 617 | }, 618 | record(unknown as unknown as Parser, p) 619 | ) 620 | 621 | const array = arrayImpl as Array 622 | 623 | 624 | // ---------------------------------------------------------------------------------------------------- 625 | 626 | type Type = 627 | 628 | (t: T) => 629 | Parser 630 | 631 | type Types = 632 | { string: string 633 | , number: number 634 | , boolean: boolean 635 | , symbol: symbol 636 | , bigint: bigint 637 | } 638 | 639 | type TypeImpl = 640 | (t: keyof Types) => 641 | Parser 642 | 643 | const typeImpl: TypeImpl = t => function*(a) { 644 | let eP = 645 | (this as ParserThis)?.typeName !== undefined 646 | ? `is not of type '${(this as ParserThis)?.typeName!}' as it ` 647 | : "" 648 | 649 | if (typeof a !== t) { yield { type: "error", value: eP + `is not of type '${t}'` }; return } 650 | yield { type: "ok", value: a as Types[keyof Types] } 651 | } 652 | 653 | const type = typeImpl as unknown as Type 654 | 655 | const string = type("string") 656 | const number = type("number") 657 | const boolean = type("boolean") 658 | const symbol = type("symbol") 659 | const bigint = type("bigint") 660 | 661 | 662 | 663 | // ---------------------------------------------------------------------------------------------------- 664 | 665 | type Value = 666 | 667 | (e: E) => 668 | Parser 669 | 670 | const value = (e => function*(a) { 671 | let eP = 672 | (this as ParserThis)?.typeName !== undefined 673 | ? `is not of type '${(this as ParserThis)?.typeName!}' as it ` 674 | : "" 675 | let s = e === undefined ? "undefined" : e === null ? "null" : e.toString() 676 | 677 | if (a !== e) { yield { type: "error", value: eP + `is not of type '${s}'` }; return } 678 | yield { type: "ok", value: a } 679 | }) as Value 680 | 681 | const undefined_ = value(undefined) 682 | const null_ = value(null) 683 | 684 | 685 | // ---------------------------------------------------------------------------------------------------- 686 | 687 | const unknown: UnknownParser = 688 | function*(a: unknown) { yield { type: "ok", value: a } } 689 | 690 | 691 | 692 | // ---------------------------------------------------------------------------------------------------- 693 | 694 | type Predicate = 695 | 696 | (isT: ((t: A) => t is T) | ((t: A) => boolean)) => 697 | Parser 698 | 699 | type PredicateImpl = 700 | (isT: (t: _A) => t is _A & _T) => 701 | Parser<_A & _T, _A> 702 | 703 | const predicateImpl: PredicateImpl = isT => function*(a) { 704 | if (isT(a)) { yield { type: "ok", value: a }; return } 705 | yield { type: "error", value: `is not of type '${(this as ParserThis)?.typeName ?? ""}'` } 706 | } 707 | 708 | const predicate = predicateImpl as unknown as Predicate 709 | 710 | 711 | 712 | // ---------------------------------------------------------------------------------------------------- 713 | 714 | type Then = 715 | { 716 | (t: Parser, u: Parser): 717 | Parser 718 | 719 | , < T extends UnknownParser 720 | , U extends Parser> 721 | > 722 | (t: T, u: U): 723 | // TODO: perhaps we don't need the `Parsed &` part? 724 | ParserAsserted & Parsed, Parsee> 725 | } 726 | 727 | type ThenImpl = 728 | (t: Parser<_A & _T, _A>, u: Parser<_A & _T & _U, _A & _T>) => Parser<_A & _T & _U, _A> 729 | 730 | const thenImpl: ThenImpl = (t, u) => function*(a) { 731 | let isT = true 732 | for (let y of t.bind(this)(a)) { 733 | if (!y) continue; 734 | if (y.type === "error") { isT = false; yield y; continue } 735 | if (y.type === "ok") continue 736 | if (y.type === "innerOk") { yield y; continue } 737 | assertNever(y) 738 | } 739 | if (!isT) return 740 | yield* u.bind(this)(a as _A & _T) 741 | } 742 | 743 | const then = thenImpl as unknown as Then 744 | 745 | 746 | 747 | // ---------------------------------------------------------------------------------------------------- 748 | 749 | type Name = 750 | { 751 | (typeName: string, parser: T): T 752 | 753 | , = Parsed> 754 | ( typeName: string 755 | , typescriptTypeName: (i: Parsed) => N 756 | , parser: T 757 | ): 758 | ParserAsserted> 759 | } 760 | 761 | const name_ = ((typeName: string, ...a: [() => void, UnknownParser] | [UnknownParser]) => 762 | Object.assign((a.length === 1 ? a[0] : a[1]).bind({ typeName }), { typeName })) as Name 763 | 764 | type N = 765 | T 766 | 767 | 768 | // ---------------------------------------------------------------------------------------------------- 769 | 770 | type Is = 771 | > 772 | (value: T, parser: P) => 773 | value is Parsed

774 | 775 | const is = ((v, p) => { 776 | for (let y of p(v)) { 777 | if (y?.type === "error") return false 778 | if (y?.type === "ok") return true 779 | } 780 | }) as Is 781 | 782 | 783 | 784 | // ---------------------------------------------------------------------------------------------------- 785 | 786 | type Assert = 787 | > 788 | (value: T, parser: P) => 789 | asserts value is Parsed

790 | 791 | const assert: Assert = ((v, p) => { 792 | for (let y of p(v)) if (y?.type === "error") throw new Error(y.value) 793 | }) 794 | 795 | 796 | 797 | // ---------------------------------------------------------------------------------------------------- 798 | 799 | const bindLazy = 800 |

801 | (f: () => P) => { 802 | let p: P | undefined 803 | 804 | return Object.defineProperty( 805 | function(this: unknown, a: never) { 806 | if (!p) p = f() 807 | return p.bind(this)(a) 808 | }, 809 | "typeName", { 810 | get: () => { 811 | if (!p) p = f() 812 | return p.typeName 813 | }, 814 | set: v => { 815 | if (!p) p = f() 816 | p.typeName = v 817 | } 818 | } 819 | ) as P 820 | } 821 | 822 | // ---------------------------------------------------------------------------------------------------- 823 | 824 | const assertNever = 825 | (() => {}) as (x?: never) => never 826 | 827 | type Test = 828 | T 829 | 830 | type AreEqual = 831 | (() => T extends B ? 1 : 0) extends (() => T extends A ? 1 : 0) 832 | ? true 833 | : false 834 | -------------------------------------------------------------------------------- /packages/runtime-checker/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as t from "../src/index" 2 | 3 | test("basic", () => { 4 | let tMouseEvent = t.bindLazy(() => t.name( 5 | "MouseEvent", 6 | t => { type MouseEvent = typeof t | never; return {} as MouseEvent }, 7 | 8 | t.union([tMouseDownEvent, tMouseUpEvent]) 9 | )) 10 | 11 | 12 | let tMouseDownEvent = t.bindLazy(() => t.name( 13 | "MouseDownEvent", 14 | t => { interface MouseDownEvent extends t.N {}; return {} as MouseDownEvent }, 15 | 16 | t.object({ 17 | type: t.value("MOUSE_DOWN"), 18 | x: t.then(t.number, t.intersect([tInteger, tPositive])), 19 | y: t.then(t.number, t.intersect([tInteger, tPositive])) 20 | }) 21 | )) 22 | 23 | let tMouseUpEvent = t.bindLazy(() => t.name( 24 | "MouseUpEvent", 25 | t => { interface MouseUpEvent extends t.N {}; return {} as MouseUpEvent }, 26 | 27 | t.object({ 28 | type: t.value("MOUSE_UP"), 29 | x: t.then(t.number, t.intersect([tInteger, tPositive])), 30 | y: t.then(t.number, t.intersect([tInteger, tPositive])) 31 | }) 32 | )) 33 | 34 | 35 | let tInteger = t.name("Integer", t.predicate( 36 | (x: number): x is number & Integer => Number.isInteger(x) 37 | )) 38 | const isInteger = Symbol("isInteger") 39 | interface Integer { [isInteger]: true } 40 | 41 | 42 | let tPositive = t.name("Positive", t.predicate( 43 | (x: number): x is number & Positive => x > 0 44 | )) 45 | const isPositive = Symbol("isPositive") 46 | interface Positive { [isPositive]: true } 47 | 48 | 49 | expect([...tMouseEvent({ x: 10.5, y: -20.5 })]).toMatchInlineSnapshot(` 50 | [ 51 | { 52 | "type": "error", 53 | "value": "is not of type 'MouseEvent' as it did not match any contituents, best match probably was 'MouseDownEvent' but it is missing key 'type'", 54 | }, 55 | { 56 | "type": "error", 57 | "value": "is not of type 'MouseEvent' as it did not match any contituents, best match probably was 'MouseDownEvent' but it's value at key 'x' is not of type 'Integer'", 58 | }, 59 | { 60 | "type": "error", 61 | "value": "is not of type 'MouseEvent' as it did not match any contituents, best match probably was 'MouseDownEvent' but it's value at key 'y' is not of type 'Integer'", 62 | }, 63 | { 64 | "type": "innerOk", 65 | }, 66 | { 67 | "type": "error", 68 | "value": "is not of type 'MouseEvent' as it did not match any contituents, best match probably was 'MouseDownEvent' but it's value at key 'y' is not of type 'Positive'", 69 | }, 70 | undefined, 71 | ] 72 | `) 73 | }) 74 | 75 | test("union best match", () => { 76 | const tEvent = t.name("Event", t.union([ 77 | t.name("ClickEvent", t.object({ 78 | type: t.value("click"), 79 | x: t.number, 80 | y: t.number 81 | })), 82 | t.name("KeypressEvent", t.object({ 83 | type: t.value("keypress"), 84 | code: t.number 85 | })) 86 | ])) 87 | 88 | expect([...tEvent({ code: 13 })]).toMatchInlineSnapshot(` 89 | [ 90 | { 91 | "type": "error", 92 | "value": "is not of type 'Event' as it did not match any contituents, best match probably was 'KeypressEvent' but it is missing key 'type'", 93 | }, 94 | { 95 | "type": "innerOk", 96 | }, 97 | undefined, 98 | ] 99 | `) 100 | }) 101 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "exactOptionalPropertyTypes": true, 9 | "noUncheckedIndexedAccess": true, 10 | "moduleResolution": "node", 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /twoslash-tester/generate.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { twoslasher } = require("@typescript/twoslash") 4 | const path = require("path") 5 | const fs = require("fs/promises") 6 | const { EOL } = require("os") 7 | 8 | let testFile = path.resolve(process.cwd(), process.argv[2]) 9 | 10 | async function generate() { 11 | process.stdout.write("generating... ") 12 | let source = await fs.readFile(testFile, "utf8"); 13 | let prefix = [ 14 | "declare const global: any", 15 | "declare const expect: any", 16 | "declare const test: any", 17 | ].join(EOL) + EOL; 18 | 19 | let twoSlashQueries = minimalTwoSlashQueries(twoslasher( 20 | prefix + source, "ts", { vfsRoot: path.dirname(testFile) } 21 | )); 22 | 23 | let { imports, body } = parseSource(source) 24 | let generatedSource = [ 25 | imports, 26 | "// @ts-ignore", 27 | "global.twoSlashQueries = getTwoSlashQueries()", 28 | body, 29 | "function getTwoSlashQueries() {", 30 | " return (", 31 | JSON.stringify(twoSlashQueries, null, " ") 32 | .split("\n") 33 | .map(l => " " + l) 34 | .join(EOL), 35 | " )", 36 | "}", 37 | ].join(EOL) + EOL 38 | 39 | await fs.writeFile( 40 | path.join( 41 | path.dirname(testFile), 42 | path.basename(testFile).replace("twoslash-", "") 43 | ), 44 | generatedSource 45 | ) 46 | process.stdout.write("done.\n") 47 | } 48 | generate(); 49 | 50 | /** 51 | * @type {(result: import("@typescript/twoslash").TwoSlashReturn) => 52 | * { text?: string 53 | * , completions?: string[] 54 | * }[] 55 | * } 56 | */ 57 | function minimalTwoSlashQueries(result) { 58 | return result.queries 59 | .map(q => 60 | q.kind !== "completions" ? q : 61 | q.completions.some(c => c.name === "globalThis") 62 | ? { ...q, completions: [] } 63 | : q 64 | ) 65 | .map(q => ({ 66 | text: q.text, 67 | completions: 68 | q.completions?.map(c => c.name) 69 | })) 70 | } 71 | 72 | 73 | /** 74 | * @param source {string} 75 | */ 76 | function parseSource(source) { 77 | return source 78 | .split(EOL) 79 | .reduce((r, l) => 80 | l.startsWith("import") || 81 | (r.body === "" && l.startsWith("/*")) // to include eslint comment 82 | ? { ...r, imports: r.imports + l + EOL } 83 | : { ...r, body: r.body + l + EOL }, 84 | { imports: "", body: "" } 85 | ) 86 | } --------------------------------------------------------------------------------