Initial commit

This commit is contained in:
Endeavorance 2025-03-25 16:15:04 -04:00
commit 1baf20f6c0
9 changed files with 825 additions and 0 deletions

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

24
LICENSE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

157
README.md Normal file
View file

@ -0,0 +1,157 @@
# Parsec
A tiny (<1kb gzipped) library for parsing and validating data in TypeScript.
For more comprehensive type modeling, consider [zod](https://zod.dev)
## Install
Configure your environment to be aware of @endeavorance packages:
```toml
# Bunfig.toml
[install.scopes]
endeavorance = "https://git.astral.camp/api/packages/endeavorance/npm/"
```
Then install the package:
```sh
bun add @endeavorance/parsec
```
## API
Parsec exports the `Parse` constant which contains a set of functions for
parsing and validating data.
### Primitives
These functions can be invoked directly, passing in the value to parse.
- `Parse.unknown(value: unknown): unknown`
- `Parse.string(value: unknown): string`
- `Parse.number(value: unknown): number`
- `Parse.boolean(value: unknown): boolean`
- `Parse.null(value: unknown): null`
- `Parse.undefined(value: unknown): undefined`
- `Parse.bigint(value: unknown): bigint`
- `Parse.symbol(value: unknown): symbol`
```ts
const num = Parse.number(42);
const str = Parse.string("hello");
const wontParse = Parse.number("hello"); // throws ParseError
```
### Primitive Values
These functions can be invoked directly and parse primitive values with
additional constraints or specific meaning.
- `Parse.int(value: unknown): number`
- `Parse.nan(value: unknown): number`
```ts
const anInteger = Parse.int(42);
const willThrow = Parse.int(42.5); // throws ParseError
const itsNan = Parse.nan(NaN);
const itsNotNan = Parse.nan(42); // throws ParseError
```
### Parser Creators
These functions return a new parser function that can be used to parse values.
These functions each take an optional second parameter: a human-readable string
identifying the shape that this parser is for. This is useful for debugging but is not
required and will default to an generic version of the parser.
- `Parse.arrayOf<T>(parser: Parser<T>): (val: unknown) => T[]`
- `Parse.shape<T>(shape: T): (val: unknown) => T`
- `Parse.enum<E extends string>(values: readonly E[]): (val: unknown) => E`
- `Parse.regex(regex: RegExp): (val: unknown) => string`
- `Parse.literal(literal: unknown): (val: unknown) => literal`
- `Parse.instanceOf<T>(classDef: Class): (val: unknown) => T`
```ts
// An array of strings
const arrayOfStrings = Parse.arrayOf(Parse.string)(["hello", "world"]);
// A defined object shape
const personParser = Parse.shape({
name: Parse.string,
age: Parse.number,
isCool: Parse.boolean,
}, "Person");
const person = personParser({
name: "Alice",
age: 42,
isCool: true,
});
// An element of an enum
const MOODS = ["happy", "sad", "angry"] as const;
const moodParser = Parse.enum(MOODS);
const mood = moodParser("happy");
// A string that matches a regex
const allCaps = Parse.regex(/^[A-Z]+$/)("HELLO");
// A literal value
const parseHello = Parse.literal("hello");
const parsedHello = parseHello("hello");
const wontWork = parseHello("world"); // throws ParseError
// An instance
const parseDate = Parse.instanceOf(Date)(new Date());
```
### Modifiers
Use `Parse.nullable()` and `Parse.optional()` to wrap parsers to allow for `null` or `undefined` respectively:
```ts
const numberOrNull = Parse.nullable(Parse.number);
const optionalArrayOfStrings = Parse.optional(Parse.arrayOf(Parse.string)));
```
### Extracting Types from Parsers
When composing parsers to create more complex types, you can use the `ReturnType`
type helper:
```ts
const parsePerson = Parse.shape({
name: Parse.string,
age: Parse.number,
isCool: Parse.boolean,
});
type Person = ReturnType<typeof parsePerson>;
```
## Errors
Parsec will throw `ParseError` errors when parsing fails.
`ParseError` objects have a string `shape` property which contains the
expected type of the value.
## Branding
Parsec exports a helper type, `Brand<T, B>` which brands type T with the
label provided in B. This is useful for creating custom types that are
primitive types under the hood but have a different name.
```ts
import type { Brand } from "@endeavorance/parsec";
type NegativeInteger = Brand<number, "NegativeInteger">;
function parseInteger(val: unknown): Integer {
if (typeof val !== "number" || !Number.isInteger(val) || val > 0) {
throw new ParseTypeMismatch("Integer", val);
}
return val as NegativeInteger;
}
```

31
biome.json Normal file
View file

@ -0,0 +1,31 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": ["dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

46
bun.lock Normal file
View file

@ -0,0 +1,46 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "parsit",
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
"@types/bun": ["@types/bun@1.2.6", "", { "dependencies": { "bun-types": "1.2.6" } }, "sha512-fY9CAmTdJH1Llx7rugB0FpgWK2RKuHCs3g2cFDYXUutIy1QGiPQxKkGY8owhfZ4MXWNfxwIbQLChgH5gDsY7vw=="],
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"bun-types": ["bun-types@1.2.6", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-FbCKyr5KDiPULUzN/nm5oqQs9nXCHD8dVc64BArxJadCvbNzAI6lUWGh9fSJZWeDIRD38ikceBU8Kj/Uh+53oQ=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
}
}

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "@endeavorance/parsec",
"version": "0.3.0",
"author": "Endeavorance",
"type": "module",
"module": "dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist/index.js",
"dist/index.d.ts"
],
"scripts": {
"clean": "rm -rf dist",
"fmt": "biome check --fix",
"build": "bun run clean && bun build ./src/index.ts --minify --outfile ./dist/index.js && bun --bunx tsc"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

246
src/index.ts Normal file
View file

@ -0,0 +1,246 @@
declare const __parsed_brand: unique symbol;
export type Brand<T, B> = T & { [__parsed_brand]: B };
export type Parser<T> = (value: unknown) => T;
export class ParseError extends Error {
name = "ParseError";
shape: string;
path: string;
constructor(message: string, shape = "unknown", path = "") {
super(message);
this.shape = shape;
this.path = path;
}
}
const MismatchError = (expectedType: string, receivedValue: unknown) => {
return new ParseError(
`Expected: ${expectedType}, Received: ${typeof receivedValue} ${JSON.stringify(receivedValue)}`,
expectedType,
);
};
type TypeofType = "number" | "string" | "boolean" | "bigint" | "symbol";
const assertTypeof = <T>(val: unknown, expectedType: TypeofType): T => {
// biome-ignore lint/suspicious/useValidTypeof: Safe through types
if (typeof val !== expectedType) {
throw MismatchError(expectedType, val);
}
return val as T;
};
export const Parse = {
unknown(val: unknown): unknown {
return val;
},
number(val: unknown): number {
return assertTypeof<number>(val, "number");
},
string(val: unknown): string {
return assertTypeof<string>(val, "string");
},
boolean(val: unknown): boolean {
return assertTypeof<boolean>(val, "boolean");
},
bigint(val: unknown): bigint {
return assertTypeof<bigint>(val, "bigint");
},
symbol(val: unknown): symbol {
return assertTypeof<symbol>(val, "symbol");
},
null(val: unknown): null {
if (val === null) {
return val;
}
throw MismatchError("null", val);
},
undefined(val: unknown): undefined {
if (val === undefined) {
return val;
}
throw MismatchError("undefined", val);
},
literal<
T extends string | number | boolean | bigint | symbol | null | undefined,
>(literal: T): (val: unknown) => T {
return (val: unknown) => {
if (val === literal) {
return val as T;
}
throw MismatchError(String(literal), val);
};
},
int(val: unknown): number {
const asNum = Parse.number(val);
if (!Number.isInteger(asNum)) {
throw MismatchError("integer", asNum);
}
return asNum;
},
nan(val: unknown): number {
if (typeof val === "number" && Number.isNaN(val)) {
return val;
}
throw MismatchError("NaN", val);
},
instanceOf<T>(classDef: { new(): T }): (val: unknown) => T {
return (val: unknown) => {
if (val instanceof classDef) {
return val;
}
throw MismatchError(`instanceof ${classDef.name}`, val);
};
},
arrayOf<T>(
parser: (val: unknown) => T,
shapeName = "Array",
): (val: unknown) => T[] {
return (val: unknown) => {
if (Array.isArray(val)) {
return val.map((entry, index) => {
try {
return parser(entry);
} catch (error) {
if (error instanceof ParseError) {
error.path += `${shapeName}[${index}]`;
throw error;
}
throw error;
}
});
}
throw MismatchError(shapeName, val);
};
},
// biome-ignore lint/suspicious/noExplicitAny: Could be fixed; intentionally kept simpler
shape<T extends Record<string, Parser<any>>>(
parsers: T,
shapeName = "Shape",
): (val: unknown) => { [K in keyof T]: ReturnType<T[K]> } {
return (val: unknown) => {
if (val === null || typeof val !== "object") {
throw MismatchError("object", val);
}
const result = {} as { [K in keyof T]: ReturnType<T[K]> };
for (const key in parsers) {
if (!(key in val)) {
throw new ParseError(`Missing prop: ${key}`, shapeName);
}
const value = (val as Record<string, unknown>)[key];
try {
// biome-ignore lint/style/noNonNullAssertion: Logically guaranteed
result[key] = parsers[key]!(value);
} catch (error) {
if (error instanceof ParseError) {
error.path += `${shapeName}.${key}`;
throw error;
}
throw error;
}
}
return result;
};
},
regex<T extends string>(
regex: RegExp,
shapeName = "Regex String",
): (val: unknown) => T {
return (val: unknown) => {
const asStr = Parse.string(val);
if (regex.test(asStr)) {
return val as T;
}
throw MismatchError(`${shapeName} (${regex.toString()})`, asStr);
};
},
enum<E extends string>(
enumDef: readonly E[],
shapeName = "Enum",
): (val: unknown) => E {
return (val: unknown) => {
if (typeof val !== "string") {
throw MismatchError("string", val);
}
if (!enumDef.includes(val as E)) {
throw MismatchError(shapeName, val);
}
return val as E;
};
},
optional<T>(parser: (val: unknown) => T): (val: unknown) => T | undefined {
return (val: unknown) => {
if (val === undefined) {
return undefined;
}
return parser(val);
};
},
nullable<T>(parser: (val: unknown) => T): (val: unknown) => T | null {
return (val: unknown) => {
if (val === null) {
return null;
}
return parser(val);
};
},
oneOf<T extends Parser<unknown>[]>(
parsers: T,
shapeName = "Union",
): (value: unknown) => ReturnType<T[number]> {
return (value: unknown): ReturnType<T[number]> => {
const errors: Error[] = [];
for (const parser of parsers) {
try {
return parser(value) as ReturnType<T[number]>;
} catch (error) {
errors.push(
error instanceof Error ? error : new Error(String(error)),
);
}
}
throw MismatchError(shapeName, value);
};
},
} as const;

228
src/test/parse.test.ts Normal file
View file

@ -0,0 +1,228 @@
import { describe, expect, test } from "bun:test";
import { Parse, ParseError } from "../index.ts";
describe(Parse.unknown, () => {
test("returns the value as provided", () => {
const val = Parse.unknown("hello");
expect(val).toBe("hello");
});
});
describe(Parse.string, () => {
test("Parses strings", () => {
expect(Parse.string("hello")).toBe("hello");
});
test("Throws an error when parsing a non-string", () => {
const INVALID_VALUES = [null, undefined, 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.string(val)).toThrow(ParseError);
}
});
});
describe(Parse.number, () => {
test("Parses numbers", () => {
expect(Parse.number(123)).toBe(123);
});
test("Throws an error when parsing a non-number", () => {
const INVALID_VALUES = [null, undefined, "hello", true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.number(val)).toThrow(ParseError);
}
});
});
describe(Parse.boolean, () => {
test("Parses booleans", () => {
expect(Parse.boolean(true)).toBe(true);
expect(Parse.boolean(false)).toBe(false);
});
test("Throws an error when parsing a non-boolean", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.boolean(val)).toThrow(ParseError);
}
});
});
describe(Parse.bigint, () => {
test("Parses bigints", () => {
expect(Parse.bigint(BigInt(123))).toBe(BigInt(123));
});
test("Throws an error when parsing a non-bigint", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.bigint(val)).toThrow(ParseError);
}
});
});
describe(Parse.symbol, () => {
test("Parses symbols", () => {
const symbol = Symbol("hello");
expect(Parse.symbol(symbol)).toBe(symbol);
});
test("Throws an error when parsing a non-symbol", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.symbol(val)).toThrow(ParseError);
}
});
});
describe(Parse.null, () => {
test("Parses null", () => {
expect(Parse.null(null)).toBe(null);
});
test("Throws an error when parsing a non-null", () => {
const INVALID_VALUES = [undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.null(val)).toThrow(ParseError);
}
});
});
describe(Parse.undefined, () => {
test("Parses undefined", () => {
expect(Parse.undefined(undefined)).toBe(undefined);
});
test("Throws an error when parsing a non-undefined", () => {
const INVALID_VALUES = [null, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.undefined(val)).toThrow(ParseError);
}
});
});
describe(Parse.arrayOf, () => {
test("Parses arrays of a specific type", () => {
const arr = [1, 2, 3];
expect(Parse.arrayOf(Parse.number)(arr)).toEqual(arr);
});
test("Throws an error when parsing a non-array", () => {
expect(() => Parse.arrayOf(Parse.number)("hello")).toThrow(ParseError);
});
test("Throws an error when parsing an array with invalid values", () => {
const INVALID_VALUES = [null, undefined, "hello", true, false, {}];
for (const val of INVALID_VALUES) {
const arr = [1, val, 3];
expect(() => Parse.arrayOf(Parse.number)(arr)).toThrow(ParseError);
}
});
});
describe(Parse.shape, () => {
test("Parses objects", () => {
const obj = { name: "hello", age: 123 };
const shape = { name: Parse.string, age: Parse.number };
expect(Parse.shape(shape)(obj)).toEqual(obj);
});
test("Throws an error when parsing a non-object", () => {
expect(() => Parse.shape({})("hello")).toThrow(ParseError);
});
test("Throws an error when parsing an object with invalid values", () => {
const obj = { name: "hello", age: "world" };
const shape = { name: Parse.string, age: Parse.number };
expect(() => Parse.shape(shape)(obj)).toThrow(ParseError);
});
});
describe(Parse.enum, () => {
test("Parses valid enum values", () => {
const enumValues = ["hello", "world"] as const;
expect(Parse.enum(enumValues)("hello")).toBe("hello");
});
test("Throws an error when parsing an invalid enum value", () => {
const enumValues = ["hello", "world"] as const;
expect(() => Parse.enum(enumValues)("invalid")).toThrow(ParseError);
});
});
describe(Parse.regex, () => {
test("Parses valid regex values", () => {
expect(Parse.regex(/hello/)("hello")).toBe("hello");
});
test("Throws an error when parsing an invalid regex value", () => {
expect(() => Parse.regex(/hello/)("world")).toThrow(ParseError);
});
});
describe(Parse.optional, () => {
test("Parses undefined values", () => {
expect(Parse.optional(Parse.string)(undefined)).toBeUndefined();
});
});
describe(Parse.nullable, () => {
test("Parses null values", () => {
expect(Parse.nullable(Parse.string)(null)).toBeNull();
});
});
describe(Parse.nan, () => {
test("Parses NaN", () => {
expect(Parse.nan(Number.NaN)).toBe(Number.NaN);
});
test("Throws an error when parsing a non-NaN", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.nan(val)).toThrow(ParseError);
}
});
});
class TestClass { }
describe(Parse.instanceOf, () => {
test("Parses instances of a class", () => {
const testClass = new TestClass();
expect(Parse.instanceOf(TestClass)(testClass)).toBe(testClass);
});
test("Throws an error when parsing a non-instance", () => {
const INVALID_VALUES = [null, undefined, "hello", 123, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.instanceOf(TestClass)(val)).toThrow(ParseError);
}
});
test("Throws an error when parsing an instance of a different class", () => {
const testClass = new TestClass();
expect(() => Parse.instanceOf(class AnotherClass { })(testClass)).toThrow(
ParseError,
);
});
});
describe(Parse.oneOf, () => {
test("Parses values that match one of the parsers", () => {
const parsers = [Parse.string, Parse.number];
expect(Parse.oneOf(parsers)("hello")).toEqual("hello");
});
test("Throws an error when parsing a value that doesn't match any parser", () => {
const parsers = [Parse.string, Parse.number];
const INVALID_VALUES = [null, undefined, true, false, {}];
for (const val of INVALID_VALUES) {
expect(() => Parse.oneOf(parsers)(val)).toThrow(ParseError);
}
});
});

30
tsconfig.json Normal file
View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": [
"esnext"
],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"allowJs": false,
// Bundler mode
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"declaration": true,
"emitDeclarationOnly": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": true,
// Files
"outDir": "dist",
"rootDir": "./src"
}
}