From 2fae2f05b9c9f0f171ac91bfd0ef348a928c222a Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Thu, 17 Jul 2025 00:02:11 -0400 Subject: [PATCH 1/4] initial commit of flinch --- package.json | 3 + packages/flinch/.gitignore | 3 + packages/flinch/LICENSE | 21 ++ packages/flinch/README.md | 359 ++++++++++++++++++++++++ packages/flinch/package.json | 53 ++++ packages/flinch/src/index.ts | 252 +++++++++++++++++ packages/flinch/src/test.ts | 502 ++++++++++++++++++++++++++++++++++ packages/flinch/tsconfig.json | 28 ++ test/types.ts | 75 +++++ 9 files changed, 1296 insertions(+) create mode 100644 packages/flinch/.gitignore create mode 100644 packages/flinch/LICENSE create mode 100644 packages/flinch/README.md create mode 100644 packages/flinch/package.json create mode 100644 packages/flinch/src/index.ts create mode 100644 packages/flinch/src/test.ts create mode 100644 packages/flinch/tsconfig.json create mode 100644 test/types.ts diff --git a/package.json b/package.json index 16ae99e..86c0c2a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,9 @@ "build-browser": "esbuild --bundle --outfile=dist/index.js --format=esm src/index.ts", "test": "node --import tsx --test test/*.ts" }, + "workspaces": [ + "packages/*" + ], "repository": { "type": "git", "url": "git+ssh://git@github.com/flickr/flickr-sdk.git" diff --git a/packages/flinch/.gitignore b/packages/flinch/.gitignore new file mode 100644 index 0000000..cf15a8c --- /dev/null +++ b/packages/flinch/.gitignore @@ -0,0 +1,3 @@ +dist/ +dist-esm/ +node_modules/ diff --git a/packages/flinch/LICENSE b/packages/flinch/LICENSE new file mode 100644 index 0000000..beff2a7 --- /dev/null +++ b/packages/flinch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 SmugMug, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 OR COPYRIGHT HOLDERS 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. \ No newline at end of file diff --git a/packages/flinch/README.md b/packages/flinch/README.md new file mode 100644 index 0000000..2d57dcd --- /dev/null +++ b/packages/flinch/README.md @@ -0,0 +1,359 @@ +# flinch 🎯 + +> almost certainly the best typescript type testing library in the world. + +flinch is a zero-dependency typescript library for compile-time type testing. it provides utilities to assert type relationships, detect type properties, and validate complex type scenarios at compile time. + +## table of contents 📚 + +- [installation](#installation) +- [quick start](#quick-start) +- [flinch object methods](#flinch-object-methods) +- [core type utilities](#core-type-utilities) +- [type detection utilities](#type-detection-utilities) +- [property analysis utilities](#property-analysis-utilities) +- [function type utilities](#function-type-utilities) +- [advanced type utilities](#advanced-type-utilities) +- [tuple and array utilities](#tuple-and-array-utilities) +- [literal type utilities](#literal-type-utilities) +- [assertion functions](#assertion-functions) +- [examples](#examples) +- [license](#license) + +## installation 📦 + +```bash +npm install @flickr/flinch +``` + +## quick start 🚀 + +```typescript +import flinch, { expectTrue, expectFalse, Equal, Extends } from '@flickr/flinch' + +// these will compile successfully +expectTrue>() +expectTrue>() +expectFalse>() + +// these will cause typescript compilation errors +expectTrue>() // ❌ compilation error +expectFalse>() // ❌ compilation error + +// these will compile successfully +flinch.equal() // ✅ passes +flinch.extends<'hello', string>() // ✅ passes + +// these will cause typescript compilation errors +flinch.equal() // ❌ compilation error +``` +## flinch object methods + +the main interface of `flinch` exposes assertion methods for convenient testing (causes compilation errors on fail). + +```typescript +import assert from '@flickr/flinch' + +assert.equal() +assert.notEqual() +assert.extends<'hello', string>() +assert.notExtends() +assert.isAny() +assert.isNever() +assert.isUnknown() +assert.isFunction<() +assert.isArray() +assert.isObject<{ a: string }>() +assert.hasProperty<{ a: string }, 'a'>() +assert.isOptional<{ a?: string }, 'a'>() +assert.isReadonly<{ readonly a: string }, 'a'>() +``` + +## core type utilities 🔧 + +### `Equal` +tests if two types are exactly equal. + +```typescript +type Test1 = Equal // true +type Test2 = Equal // false +``` + +### `NotEqual` +tests if two types are not equal. + +```typescript +type Test1 = NotEqual // true +type Test2 = NotEqual // false +``` + +### `Extends` +tests if type X extends type Y. + +```typescript +type Test1 = Extends<'hello', string> // true +type Test2 = Extends // false +``` + +### `NotExtends` +tests if type X does not extend type Y. + +```typescript +type Test1 = NotExtends // true +type Test2 = NotExtends<'hello', string> // false +``` + +## type detection utilities 🔍 + +### `IsAny` +detects if a type is `any`. + +```typescript +type Test1 = IsAny // true +type Test2 = IsAny // false +``` + +### `IsNever` +detects if a type is `never`. + +```typescript +type Test1 = IsNever // true +type Test2 = IsNever // false +``` + +### `IsUnknown` +detects if a type is `unknown`. + +```typescript +type Test1 = IsUnknown // true +type Test2 = IsUnknown // false +``` + +### `IsFunction` +detects if a type is a function. + +```typescript +type Test1 = IsFunction<() => void> // true +type Test2 = IsFunction // false +``` + +### `IsArray` +detects if a type is an array. + +```typescript +type Test1 = IsArray // true +type Test2 = IsArray // false +``` + +### `IsObject` +detects if a type is an object (excluding arrays and functions). + +```typescript +type Test1 = IsObject<{ a: string }> // true +type Test2 = IsObject // false +``` + +## property analysis utilities 🔬 + +### `HasProperty` +checks if a type has a specific property. + +```typescript +interface User { name: string; age?: number } +type Test1 = HasProperty // true +type Test2 = HasProperty // false +``` + +### `IsOptional` +checks if a property is optional. + +```typescript +interface User { name: string; age?: number } +type Test1 = IsOptional // true +type Test2 = IsOptional // false +``` + +### `IsReadonly` +checks if a property is readonly. + +```typescript +interface User { name: string; readonly id: number } +type Test1 = IsReadonly // true +type Test2 = IsReadonly // false +``` + +## function type utilities ⚡ + +### `GetParameters` +extracts parameter types from a function type. + +```typescript +type Fn = (a: string, b: number) => void +type Params = GetParameters // [string, number] +``` + +### `GetReturnType` +extracts the return type from a function type. + +```typescript +type Fn = (a: string) => number +type Return = GetReturnType // number +``` + +## advanced type utilities 🧠 + +### `IsUnion` +detects if a type is a union type. + +```typescript +type Test1 = IsUnion // true +type Test2 = IsUnion // false +``` + +### `UnionToIntersection` +converts a union type to an intersection type. + +```typescript +type Union = { a: string } | { b: number } +type Intersection = UnionToIntersection // { a: string } & { b: number } +``` + +## tuple and array utilities 📋 + +### `IsTuple` +detects if a type is a tuple (fixed-length array). + +```typescript +type Test1 = IsTuple<[string, number]> // true +type Test2 = IsTuple // false +``` + +### `TupleLength` +gets the length of a tuple type. + +```typescript +type Length = TupleLength<[string, number, boolean]> // 3 +``` + +### `Head` +gets the first element type of a tuple. + +```typescript +type First = Head<[string, number, boolean]> // string +``` + +### `Tail` +gets all elements except the first from a tuple. + +```typescript +type Rest = Tail<[string, number, boolean]> // [number, boolean] +``` + +## literal type utilities 📝 + +### `IsStringLiteral` +detects if a type is a string literal. + +```typescript +type Test1 = IsStringLiteral<'hello'> // true +type Test2 = IsStringLiteral // false +``` + +### `IsNumberLiteral` +detects if a type is a number literal. + +```typescript +type Test1 = IsNumberLiteral<42> // true +type Test2 = IsNumberLiteral // false +``` + +## assertion functions 🎪 + +flinch provides both type-level utilities and runtime assertion functions: + +### `expectTrue()` +asserts that a type condition is true (causes compilation error if false). + +```typescript +expectTrue>() // ✅ compiles +expectTrue>() // ❌ compilation error +``` + +### `expectFalse()` +asserts that a type condition is false (causes compilation error if true). + +```typescript +expectFalse>() // ✅ compiles +expectFalse>() // ❌ compilation error +``` + +## examples 💡 + +### testing api response types + +```typescript +import { expectTrue, Equal, HasProperty, IsOptional } from '@flickr/flinch' + +interface ApiResponse { + data: T + status: number + message?: string +} + +interface User { + id: number + name: string + email?: string +} + +type UserResponse = ApiResponse + +// test the structure +expectTrue>() +expectTrue>() +expectTrue>() + +// test nested properties +expectTrue>() +expectTrue>() +expectTrue>() +``` + +### testing utility types + +```typescript +import { expectTrue, Equal, IsUnion, IsTuple } from '@flickr/flinch' + +// test conditional types +type NonNullable = T extends null | undefined ? never : T + +expectTrue, string>>() +expectTrue, string>>() + +// test mapped types +type Partial = { [P in keyof T]?: T[P] } + +interface Original { a: string; b: number } +type PartialOriginal = Partial + +expectTrue>() +expectTrue>() +``` + +### testing failure cases + +use `@ts-expect-error` to tell typescript to expect a compilation error. +if an error is not produced, typescript will complain. + +```typescript +import assert from '@flickr/flinch' + +assert.extends() // ❌ compilation error + +// @ts-expect-error - string is not number +assert.extends() // ✅ compiles +``` + +## license 📄 + +MIT © SmugMug, Inc diff --git a/packages/flinch/package.json b/packages/flinch/package.json new file mode 100644 index 0000000..d7700c9 --- /dev/null +++ b/packages/flinch/package.json @@ -0,0 +1,53 @@ +{ + "name": "@flickr/flinch", + "version": "1.0.0", + "description": "Almost certainly the best Typescript type testing library in the world.", + "keywords": [ + "typescript", + "types", + "test", + "testing", + "zero-dependency", + "flickr" + ], + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "tsc && npm run build-esm", + "build-esm": "tsc --module esnext --outDir dist-esm && mv dist-esm/index.js dist/index.mjs", + "test": "tsc --noEmit src/test.ts" + }, + "repository": { + "type": "git", + "url": "github:flickr/flickr-sdk", + "directory": "packages/flinch" + }, + "author": "Jeremy Ruppel", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "devDependencies": { + "typescript": "^5.2.2" + } +} + diff --git a/packages/flinch/src/index.ts b/packages/flinch/src/index.ts new file mode 100644 index 0000000..d6cd785 --- /dev/null +++ b/packages/flinch/src/index.ts @@ -0,0 +1,252 @@ +// Core type testing utilities +export type Expect = T +export type Equal = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 + ? true + : false +export type NotEqual = Equal extends true ? false : true +export type Extends = X extends Y ? true : false +export type NotExtends = Extends extends true ? false : true + +// Utility types for common type checks +export type IsAny = 0 extends 1 & T ? true : false +export type IsNever = [T] extends [never] ? true : false +export type IsUnknown = + IsAny extends true + ? false + : Equal extends true + ? true + : false +export type IsFunction = T extends (...args: any[]) => any + ? true + : T extends Function + ? true + : false +export type IsArray = T extends readonly any[] ? true : false +export type IsObject = T extends object + ? T extends any[] + ? false + : T extends (...args: any[]) => any + ? false + : T extends Function + ? false + : true + : false + +// Advanced type checks +export type HasProperty = K extends keyof T + ? true + : false +export type PropertyType = T[K] +export type IsOptional = + {} extends Pick ? true : false +export type IsReadonly = + Equal<{ -readonly [P in K]: T[P] }, { [P in K]: T[P] }> extends true + ? false + : true + +// Function type utilities +export type GetParameters any> = T extends ( + ...args: infer P +) => any + ? P + : never +export type GetReturnType any> = T extends ( + ...args: any +) => infer R + ? R + : any + + +// Advanced type utilities for complex scenarios +export type IsUnion = [T] extends [UnionToIntersection] ? false : true +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never +export type UnionToTuple = + UnionToIntersection T : never> extends () => infer W + ? [...UnionToTuple>, W] + : [] + +// Conditional type utilities +export type IsConditional = T extends any ? true : false +export type IsMappedType = T extends { [K in keyof T]: T[K] } ? true : false + + + +// Tuple and array utilities +export type IsTuple = T extends readonly any[] + ? number extends T["length"] + ? false + : true + : false +export type TupleLength = T["length"] +export type Head = T extends readonly [ + infer H, + ...any[], +] + ? H + : never +export type Tail = T extends readonly [ + any, + ...infer Rest, +] + ? Rest + : never + +// String literal type utilities +export type IsStringLiteral = T extends string + ? string extends T + ? false + : true + : false +export type IsNumberLiteral = T extends number + ? number extends T + ? false + : true + : false + +/** + * Assertion functions for compile-time type checking + * These functions will cause TypeScript compilation + * errors if the type assertions fail + */ +const flinch = { + /** + * Assert that a type equals another type + */ + equal(this: Equal extends true ? void : never): void {}, + + /** + * Assert that a type does not equal another type + */ + notEqual(this: NotEqual extends true ? void : never): void {}, + + /** + * Assert that a type extends another type + */ + extends(this: Extends extends true ? void : never): void {}, + + /** + * Assert that a type does not extend another type + */ + notExtends(this: NotExtends extends true ? void : never): void {}, + + /** + * Assert that a type is any + */ + isAny(this: IsAny extends true ? void : never): void {}, + + /** + * Assert that a type is never + */ + isNever(this: IsNever extends true ? void : never): void {}, + + /** + * Assert that a type is unknown + */ + isUnknown(this: IsUnknown extends true ? void : never): void {}, + + /** + * Assert that a type is a function + */ + isFunction(this: IsFunction extends true ? void : never): void {}, + + /** + * Assert that a type is an array + */ + isArray(this: IsArray extends true ? void : never): void {}, + + /** + * Assert that a type is an object (but not array) + */ + isObject(this: IsObject extends true ? void : never): void {}, + + /** + * Assert that a type has a specific property + */ + hasProperty(this: HasProperty extends true ? void : never): void {}, + + /** + * Assert that a property is optional + */ + isOptional(this: IsOptional extends true ? void : never): void {}, + + /** + * Assert that a property is readonly + */ + isReadonly(this: IsReadonly extends true ? void : never): void {}, +} + +/** + * Compile-time type assertion that will cause TypeScript + * errors if the condition is false + * Usage: expectTrue>() // OK + * expectTrue>() // TypeScript error + */ +export function expectTrue(): void { + // Compile-time only assertion +} + +/** + * Compile-time type assertion that will cause TypeScript + * errors if the condition is true + * Usage: expectFalse>() // OK + * expectFalse>() // TypeScript error + */ +export function expectFalse(): void { + // Compile-time only assertion +} + +/** + * Advanced type assertion utilities + */ + +/** + * Assert that a type is a union type + */ +export function isUnion(): IsUnion extends true ? void : never { + return undefined as IsUnion extends true ? void : never +} + +/** + * Assert that a type is a tuple (fixed-length array) + */ +export function isTuple(): IsTuple extends true ? void : never { + return undefined as IsTuple extends true ? void : never +} + +/** + * Assert that a tuple has a specific length + */ +export function tupleLength< + T extends readonly any[], + N extends number, +>(): TupleLength extends N ? void : never { + return undefined as TupleLength extends N ? void : never +} + +/** + * Assert that a type is a string literal + */ +export function isStringLiteral(): IsStringLiteral extends true + ? void + : never { + return undefined as IsStringLiteral extends true ? void : never +} + +/** + * Assert that a type is a number literal + */ +export function isNumberLiteral(): IsNumberLiteral extends true + ? void + : never { + return undefined as IsNumberLiteral extends true ? void : never +} + + + +// Make flinch the default export +export default flinch diff --git a/packages/flinch/src/test.ts b/packages/flinch/src/test.ts new file mode 100644 index 0000000..c66fece --- /dev/null +++ b/packages/flinch/src/test.ts @@ -0,0 +1,502 @@ +import { describe, it } from "node:test" +import flinch, { + expectTrue, + expectFalse, + type Equal, + type NotEqual, + type Extends, + type NotExtends, + type IsAny, + type IsNever, + type IsUnknown, + type IsFunction, + type IsArray, + type IsObject, + type HasProperty, + type IsOptional, + type IsReadonly, + type IsUnion, + type IsTuple, + type TupleLength, + type Head, + type Tail, + type IsStringLiteral, + type IsNumberLiteral, + type GetParameters, + type GetReturnType, + isStringLiteral, + isTuple, + tupleLength, + isUnion, +} from "./index" + +describe("Flinch Self-Test Suite", () => { + describe("Core Type Utilities", () => { + it("should validate Equal type utility", () => { + // Test Equal with identical types + expectTrue>() + expectTrue>() + expectTrue>() + + // Test Equal with different types + expectFalse>() + expectFalse>() + expectFalse>() + + // Test Equal with complex types + type ComplexType1 = { a: string; b: number; c?: boolean } + type ComplexType2 = { a: string; b: number; c?: boolean } + type ComplexType3 = { a: string; b: number; c: boolean } + + expectTrue>() + expectFalse>() + }) + + it("should validate NotEqual type utility", () => { + expectTrue>() + expectTrue>() + expectFalse>() + expectFalse>() + }) + + it("should validate Extends type utility", () => { + expectTrue>() + expectTrue>() + expectTrue>() + expectTrue>() + + expectFalse>() + expectFalse>() + expectFalse>() + }) + + it("should validate NotExtends type utility", () => { + expectTrue>() + expectTrue>() + expectFalse>() + expectFalse>() + }) + }) + + describe("Special Type Detection", () => { + it("should validate IsAny type utility", () => { + expectTrue>() + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + + it("should validate IsNever type utility", () => { + expectTrue>() + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + + // Test with conditional types that resolve to never + type NeverType = string & number + expectTrue>() + }) + + it("should validate IsUnknown type utility", () => { + expectTrue>() + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + }) + + describe("Structural Type Detection", () => { + it("should validate IsFunction type utility", () => { + expectTrue void>>() + expectTrue number>>() + expectTrue any>>() + expectTrue>() + + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + + it("should validate IsArray type utility", () => { + expectTrue>() + expectTrue>() + expectTrue>() + expectTrue>() + expectTrue>>() + + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + + it("should validate IsObject type utility", () => { + expectTrue>() + expectTrue>>() + expectTrue>() + expectTrue>() + + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + }) + + describe("Property Analysis", () => { + interface TestInterface { + required: string + optional?: number + readonly readonlyProp: boolean + normalProp: string + } + + it("should validate HasProperty type utility", () => { + expectTrue>() + expectTrue>() + expectTrue>() + expectTrue>() + + expectFalse>() + expectFalse>() + }) + + it("should validate IsOptional type utility", () => { + expectTrue>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + + it("should validate IsReadonly type utility", () => { + expectTrue>() + expectFalse>() + expectFalse>() + expectFalse>() + }) + }) + + describe("Function Type Utilities", () => { + type TestFunction = (a: string, b: number, c?: boolean) => Promise + type GenericFunction = (x: T) => T + type VoidFunction = () => void + + it("should validate GetParameters type utility", () => { + expectTrue< + Equal, [string, number, boolean?]> + >() + expectTrue, []>>() + + // Test with built-in function types + expectTrue, [string, number?]>>() + }) + + it("should validate GetReturnType type utility", () => { + expectTrue, Promise>>() + expectTrue, void>>() + expectTrue number>, number>>() + }) + }) + + describe("Advanced Type Utilities", () => { + it("should validate IsUnion type utility", () => { + expectTrue>() + expectTrue>() + expectTrue>() // boolean is true | false + + expectFalse>() + expectFalse>() + expectFalse>() + }) + + it("should validate IsTuple type utility", () => { + expectTrue>() + expectTrue>() + expectTrue>() + expectTrue>() + + expectFalse>() + expectFalse>() + expectFalse>>() + }) + + it("should validate TupleLength type utility", () => { + expectTrue, 2>>() + expectTrue, 3>>() + expectTrue, 0>>() + expectTrue, 1>>() + }) + + it("should validate Head and Tail type utilities", () => { + type TestTuple = [string, number, boolean] + + expectTrue, string>>() + expectTrue, [number, boolean]>>() + expectTrue, number>>() + expectTrue, []>>() + }) + + it("should validate literal type utilities", () => { + expectTrue>() + expectTrue>() + expectFalse>() + + expectTrue>() + expectTrue>() + expectTrue>() + expectFalse>() + }) + }) + + describe("Type Assertion Functions", () => { + it("should validate flinch functions work correctly", () => { + // These should compile without errors if the type assertions are correct + flinch.equal() + flinch.notEqual() + flinch.extends() + flinch.notExtends() + + flinch.isFunction<() => void>() + flinch.isArray() + flinch.isObject<{ a: string }>() + + interface TestType { + prop: string + optional?: number + readonly readonlyProp: boolean + } + + flinch.hasProperty() + flinch.isOptional() + flinch.isReadonly() + }) + }) + + describe("Complex Type Scenarios", () => { + it("should handle deeply nested generic types", () => { + interface ApiResponse { + data: T + meta: { + status: number + message?: string + } + } + + interface User { + id: number + profile: { + name: string + settings: { + theme: "light" | "dark" + notifications: boolean + } + } + } + + type UserApiResponse = ApiResponse + + // Test nested property access + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + + // Test deep type equality + flinch.equal< + UserApiResponse["data"]["profile"]["settings"]["theme"], + "light" | "dark" + >() + flinch.equal() + + // Test optional properties at different levels + flinch.isOptional() + }) + + it("should handle conditional and mapped types", () => { + // Conditional type + type NonNullable = T extends null | undefined ? never : T + + flinch.equal, string>() + flinch.equal, string>() + flinch.isNever>() + + // Mapped type + type Partial = { + [P in keyof T]?: T[P] + } + + interface Original { + a: string + b: number + } + + type PartialOriginal = Partial + + flinch.isOptional() + flinch.isOptional() + flinch.equal() + }) + + it("should handle template literal types", () => { + type EventName = `on${Capitalize}` + type ClickEvent = EventName<"click"> + type HoverEvent = EventName<"hover"> + + flinch.equal() + flinch.equal() + + // Test that these are string literals + isStringLiteral() + isStringLiteral() + }) + }) + + describe("Edge Cases and Error Conditions", () => { + it("should handle empty types correctly", () => { + type EmptyObject = {} + type EmptyTuple = [] + type EmptyUnion = never + + flinch.isObject() + isTuple() + tupleLength() + flinch.isNever() + }) + + it("should handle recursive types", () => { + interface TreeNode { + value: string + children?: TreeNode[] + } + + flinch.hasProperty() + flinch.hasProperty() + flinch.isOptional() + + // Test that children is an array of TreeNode + type ChildrenType = NonNullable + flinch.isArray() + }) + + it("should handle intersection types", () => { + interface A { + a: string + } + + interface B { + b: number + } + + type AB = A & B + + flinch.hasProperty() + flinch.hasProperty() + flinch.equal() + flinch.equal() + + // Test that AB extends both A and B + flinch.extends() + flinch.extends() + }) + }) +}) + +// Test that flinch methods properly fail with invalid types +describe("Flinch Method Failure Tests", () => { + interface TestInterface { + existingProp: string + optionalProp?: number + readonly readonlyProp: boolean + } + + it("should cause compilation errors for invalid type assertions", () => { + // These should all cause TypeScript compilation errors: + + // @ts-expect-error - string does not equal number + flinch.equal() + + // @ts-expect-error - string equals string + flinch.notEqual() + + // @ts-expect-error - string does not extend number + flinch.extends() + + // @ts-expect-error - string extends string | number + flinch.notExtends() + + // @ts-expect-error - string is not any + flinch.isAny() + + // @ts-expect-error - string is not never + flinch.isNever() + + // @ts-expect-error - string is not unknown + flinch.isUnknown() + + // @ts-expect-error - string is not a function + flinch.isFunction() + + // @ts-expect-error - string is not an array + flinch.isArray() + + // @ts-expect-error - string is not an object + flinch.isObject() + + // @ts-expect-error - arrays are not objects in our definition + flinch.isObject() + + // @ts-expect-error - functions are not objects in our definition + flinch.isObject<() => void>() + + // @ts-expect-error - property doesn't exist + flinch.hasProperty() + + // @ts-expect-error - existingProp is required, not optional + flinch.isOptional() + + // @ts-expect-error - existingProp is not readonly + flinch.isReadonly() + }) +}) + +// Test that Flinch can validate its own API +describe("Flinch API Validation", () => { + it("should validate flinch API structure", () => { + flinch.isObject() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + flinch.hasProperty() + + // All methods should be functions + flinch.isFunction() + flinch.isFunction() + flinch.isFunction() + flinch.isFunction() + }) + + it("should validate advanced type assertions", () => { + // All methods should be functions + flinch.isFunction() + flinch.isFunction() + flinch.isFunction() + flinch.isFunction() + }) +}) diff --git a/packages/flinch/tsconfig.json b/packages/flinch/tsconfig.json new file mode 100644 index 0000000..0bae053 --- /dev/null +++ b/packages/flinch/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "src/index.ts" + ], + "exclude": [ + "node_modules", + "dist", + "dist-esm" + ] +} diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 0000000..afe518c --- /dev/null +++ b/test/types.ts @@ -0,0 +1,75 @@ +import { describe, it } from "node:test" +import flinch from "flinch" +import type { Auth, Transport, Parser } from "../src/types" +import type { GET, POST, Params } from "../src/params" + +describe("flickr-sdk type tests", () => { + describe("Auth Interface", () => { + it("should have correct method signature", () => { + // Verify Auth interface structure + flinch.hasProperty() + flinch.isFunction() + + // Test the sign method parameters and return type + type SignMethod = Auth["sign"] + flinch.isFunction() + + // The sign method should return a Promise + type SignReturnType = ReturnType + flinch.extends>() + }) + }) + + describe("Transport Interface", () => { + it("should have get and post methods", () => { + flinch.hasProperty() + flinch.hasProperty() + + flinch.isFunction() + flinch.isFunction() + }) + + it("should have correct method signatures", () => { + // Test get method + type GetMethod = Transport["get"] + type GetReturnType = ReturnType + flinch.extends>() + + // Test post method + type PostMethod = Transport["post"] + type PostReturnType = ReturnType + flinch.extends>() + }) + }) + + describe("Parser Interface", () => { + it("should have parse method with correct signature", () => { + flinch.hasProperty() + flinch.isFunction() + + type ParseMethod = Parser["parse"] + type ParseReturnType = ReturnType + flinch.extends>() + }) + }) + + describe("Parameter Types", () => { + it("should verify GET and POST extend Params", () => { + flinch.extends() + flinch.extends() + }) + }) + + describe("Integration Type Tests", () => { + it("should verify interface compatibility", () => { + // Test that implementations would satisfy the interfaces + interface MockAuth extends Auth {} + interface MockTransport extends Transport {} + interface MockParser extends Parser {} + + flinch.extends() + flinch.extends() + flinch.extends() + }) + }) +}) From 181984c58f05da8b44c8d1b4dac1db9ab37744f9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Fri, 18 Jul 2025 11:23:57 -0400 Subject: [PATCH 2/4] use flinch to test types --- .github/workflows/pull-request.yaml | 10 +-- README.md | 18 ++++- package.json | 7 +- src/auth/api_key.ts | 6 +- src/index.ts | 10 ++- src/services/rest/index.ts | 2 + src/transport/mock.ts | 6 +- test/oauth.ts | 2 +- test/transport.fetch.ts | 14 ++-- test/types.ts | 108 ++++++++++++++++++++++------ tsconfig.json | 8 ++- tsconfig.test.json | 7 ++ 12 files changed, 152 insertions(+), 46 deletions(-) create mode 100644 tsconfig.test.json diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index e8211b3..7ec48a1 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -17,12 +17,14 @@ jobs: node-version: [20.x, 22.x, 24.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: npm install + - run: npm install + - run: npm install --workspaces - run: npm run build + - run: npm run build --workspaces - run: npm test + - run: npm test --workspaces diff --git a/README.md b/README.md index 1a5b4f0..cb6c88c 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,24 @@ const { flickr } = createFlickr("") ``` #### OAuth 1.0 -OAuth lets users grant your application access and then you may act on their -behalf. The OAuth flow is described [here][oauth]. +OAuth lets users grant your application access and then you may act on +their behalf. The OAuth flow is described [here][oauth]. ```js +// create a flickr upload flickr without oauth credentials. Use this to +// obtain a request token and authorize a user +const { upload } = createFlickr({ + consumerKey: "", + consumerSecret: "", + oauthToken: "", + oauthTokenSecret: "", +}) +``` + +Then, once you have your OAuth token and secret: + +```js +// create a flickr upload client with oauth credentials const { upload } = createFlickr({ consumerKey: "", consumerSecret: "", diff --git a/package.json b/package.json index 86c0c2a..9a5963b 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ ], "scripts": { "tsc": "tsc", - "build": "npm run tsc && npm run build-cjs && npm run build-esm && npm run build-browser", + "build": "tsc && npm run build-cjs && npm run build-esm && npm run build-browser", "build-cjs": "esbuild --bundle --outfile=dist/index.cjs --platform=node --format=cjs src/index.ts", "build-esm": "esbuild --bundle --outfile=dist/index.mjs --platform=node --format=esm src/index.ts", "build-browser": "esbuild --bundle --outfile=dist/index.js --format=esm src/index.ts", - "test": "node --import tsx --test test/*.ts" + "test": "tsc -p tsconfig.test.json && node --import tsx --test test/*.ts" }, "workspaces": [ "packages/*" @@ -72,6 +72,7 @@ "devDependencies": { "@types/node": "^20.8.4", "esbuild": "^0.19.4", + "@flickr/flinch": "file:./packages/flinch", "flickr-sdk": "file:.", "min-qs": "^1.4.0", "min-url": "^1.5.0", @@ -83,4 +84,4 @@ "dependencies": { "@rgrove/parse-xml": "^4.1.0" } -} +} \ No newline at end of file diff --git a/src/auth/api_key.ts b/src/auth/api_key.ts index ec4c0e3..6b9b5b9 100644 --- a/src/auth/api_key.ts +++ b/src/auth/api_key.ts @@ -1,10 +1,10 @@ import type { Auth } from "../types" import type { Params } from "../params" +export type APIKeyAuthConfig = string | (() => string) | (() => Promise) + export class APIKeyAuth implements Auth { - constructor( - private apiKey: string | (() => string) | (() => Promise), - ) { + constructor(private apiKey: APIKeyAuthConfig) { if (typeof apiKey === "undefined") { throw new Error("apiKey must be provided") } diff --git a/src/index.ts b/src/index.ts index 9ed67a6..bf4017e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import type { Auth, Transport } from "./types" -import { APIKeyAuth } from "./auth/api_key" +import type { Auth, Transport, Parser } from "./types" +import { APIKeyAuth, APIKeyAuthConfig } from "./auth/api_key" import { OAuthAuth, OAuthConfig } from "./auth/oauth" import { FlickrService, Flickr } from "./services/rest" import { OAuthService } from "./services/oauth" @@ -93,22 +93,26 @@ export function createFlickr( } } -export type { Flickr, Auth, Transport } +export type { Flickr, Auth, Transport, Parser } export { FlickrService, UploadService, ReplaceService, FetchTransport, APIKeyAuth, + APIKeyAuthConfig, OAuthAuth, OAuthService, + OAuthConfig, } export * from "./services/rest/api" // exports for tests + export { OAuth } from "./oauth" export { Params, GET, POST } from "./params" export { XMLParser } from "./parser/xml" export { JSONParser } from "./parser/json" export { FormParser } from "./parser/form" export { NullAuth } from "./auth/null" +export { API } from "./services/rest" export { MockTransport } from "./transport/mock" diff --git a/src/services/rest/index.ts b/src/services/rest/index.ts index e921048..bd68fea 100644 --- a/src/services/rest/index.ts +++ b/src/services/rest/index.ts @@ -3,6 +3,8 @@ import { POST_REGEXP, API } from "./api" import { GET, POST } from "../../params" import { JSONParser } from "../../parser/json" +export type { API } + export interface Flickr { (method: T, params: API[T][0]): Promise } diff --git a/src/transport/mock.ts b/src/transport/mock.ts index 3d6dadf..cc89a40 100644 --- a/src/transport/mock.ts +++ b/src/transport/mock.ts @@ -1,9 +1,9 @@ import { Transport } from "../types" export class MockTransport implements Transport { - private responses: string[] = [] + private responses: any[] = [] - constructor(response?: string) { + constructor(response?: any) { if (response) { this.addMock(response) } @@ -13,7 +13,7 @@ export class MockTransport implements Transport { this.responses = [] } - addMock(response: string): void { + addMock(response: any): void { const stringResponse = typeof response === "string" ? response : JSON.stringify(response) this.responses.push(stringResponse) diff --git a/test/oauth.ts b/test/oauth.ts index d670a43..bc98b92 100644 --- a/test/oauth.ts +++ b/test/oauth.ts @@ -3,7 +3,7 @@ import * as assert from "node:assert" import { OAuth } from "flickr-sdk" describe("OAuth", function () { - let oauth + let oauth: OAuth beforeEach(function () { oauth = new OAuth("consumer key", "consumer secret") diff --git a/test/transport.fetch.ts b/test/transport.fetch.ts index 22c5fed..8cc64e4 100644 --- a/test/transport.fetch.ts +++ b/test/transport.fetch.ts @@ -1,7 +1,7 @@ import { afterEach, describe, it, mock } from "node:test" import * as assert from "node:assert" import { FetchTransport, GET, POST } from "flickr-sdk" -import { createServer } from "node:http" +import { createServer, type Server, type IncomingMessage } from "node:http" import { once } from "node:events" describe("transport/fetch", function () { @@ -21,7 +21,9 @@ describe("transport/fetch", function () { const [url, init] = fn.mock.calls[0].arguments + assert.ok(url) assert.strictEqual(url, "http://example.com/foo?foo=bar") + assert.ok(init) assert.strictEqual(init.method, "GET") }) @@ -40,6 +42,8 @@ describe("transport/fetch", function () { const [url, init] = fn.mock.calls[0].arguments + assert.ok(url) + assert.ok(init) // @ts-ignore assert.strictEqual(init.headers.cookie, "foo") }) @@ -61,7 +65,9 @@ describe("transport/fetch", function () { const [url, init] = fn.mock.calls[0].arguments + assert.ok(url) assert.strictEqual(url, "http://example.com/foo") + assert.ok(init) assert.strictEqual(init.method, "POST") assert.ok(init.body instanceof FormData) assert.strictEqual(init.body.get("foo"), "bar") @@ -94,8 +100,7 @@ describe("transport/fetch", function () { res.end(JSON.stringify(body)) }) - /** @type {import("http").Server | null} */ - let server + let server: Server | null afterEach(function () { server?.close() @@ -106,8 +111,7 @@ describe("transport/fetch", function () { server = createTestServer(200) server.listen(3000) - /** @type {Promise<[import('http').IncomingMessage]>} */ - const promise = once(server, "request") + const promise: Promise = once(server, "request") const transport = new FetchTransport() diff --git a/test/types.ts b/test/types.ts index afe518c..3b90a43 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,62 +1,73 @@ import { describe, it } from "node:test" -import flinch from "flinch" -import type { Auth, Transport, Parser } from "../src/types" -import type { GET, POST, Params } from "../src/params" +import assert, { Equal, Extends, IsTuple, expectTrue } from "@flickr/flinch" +import type { + API, + Auth, + Transport, + Parser, + GET, + POST, + Params, + OAuthConfig, + FlickrPeopleGetInfoParams, + APIKeyAuthConfig, + FlickrTestEchoParams, +} from "flickr-sdk" describe("flickr-sdk type tests", () => { describe("Auth Interface", () => { it("should have correct method signature", () => { // Verify Auth interface structure - flinch.hasProperty() - flinch.isFunction() + assert.hasProperty() + assert.isFunction() // Test the sign method parameters and return type type SignMethod = Auth["sign"] - flinch.isFunction() + assert.isFunction() // The sign method should return a Promise type SignReturnType = ReturnType - flinch.extends>() + assert.extends>() }) }) describe("Transport Interface", () => { it("should have get and post methods", () => { - flinch.hasProperty() - flinch.hasProperty() + assert.hasProperty() + assert.hasProperty() - flinch.isFunction() - flinch.isFunction() + assert.isFunction() + assert.isFunction() }) it("should have correct method signatures", () => { // Test get method type GetMethod = Transport["get"] type GetReturnType = ReturnType - flinch.extends>() + assert.extends>() // Test post method type PostMethod = Transport["post"] type PostReturnType = ReturnType - flinch.extends>() + assert.extends>() }) }) describe("Parser Interface", () => { it("should have parse method with correct signature", () => { - flinch.hasProperty() - flinch.isFunction() + assert.hasProperty() + assert.isFunction() type ParseMethod = Parser["parse"] type ParseReturnType = ReturnType - flinch.extends>() + assert.extends>() }) }) describe("Parameter Types", () => { it("should verify GET and POST extend Params", () => { - flinch.extends() - flinch.extends() + assert.extends() + assert.extends() }) }) @@ -67,9 +78,64 @@ describe("flickr-sdk type tests", () => { interface MockTransport extends Transport {} interface MockParser extends Parser {} - flinch.extends() - flinch.extends() - flinch.extends() + assert.extends() + assert.extends() + assert.extends() + }) + }) + + describe("API", function () { + it("has flickr.people.getInfo", () => { + assert.hasProperty() + + expectTrue>() + expectTrue< + Equal + >() + }) + + it("has flickr.test.echo", () => { + assert.hasProperty() + + expectTrue>() + expectTrue>() + }) + }) + + describe("APIKeyConfig", () => { + it("accepts a string", () => { + expectTrue>() + expectTrue string, APIKeyAuthConfig>>() + expectTrue Promise, APIKeyAuthConfig>>() + }) + }) + + describe("OAuthConfig", () => { + it("accepts consumer key/secret and oauth token/secret", () => { + expectTrue< + Extends< + { + consumerKey: string + consumerSecret: string + oauthToken: string + oauthTokenSecret: string + }, + OAuthConfig + > + >() + }) + it("can specify false for oauth token/secret", () => { + expectTrue< + Extends< + { + consumerKey: string + consumerSecret: string + oauthToken: false + oauthTokenSecret: false + }, + OAuthConfig + > + >() }) }) }) diff --git a/tsconfig.json b/tsconfig.json index a95c9f5..56741f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,11 @@ "node" ] }, - "include": ["src/**/*"] + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..9c98d8d --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "noEmit": true + }, + "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "test/**/*.ts"] +} From 52e619f73196947dc9bf7c8873a36fdc6d156a2d Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Fri, 18 Jul 2025 16:14:55 -0400 Subject: [PATCH 3/4] don't need to use node:test for type tests --- packages/flinch/src/test.ts | 873 +++++++++++++++++------------------- test/types.ts | 234 +++++----- 2 files changed, 529 insertions(+), 578 deletions(-) diff --git a/packages/flinch/src/test.ts b/packages/flinch/src/test.ts index c66fece..ffad8cd 100644 --- a/packages/flinch/src/test.ts +++ b/packages/flinch/src/test.ts @@ -1,4 +1,11 @@ -import { describe, it } from "node:test" +/** + * Flinch Self-Test Suite + * + * This file contains compile-time type assertions to validate that Flinch's + * type testing utilities work correctly. These tests verify the framework + * by testing its own type system. + */ + import flinch, { expectTrue, expectFalse, @@ -25,478 +32,440 @@ import flinch, { type GetParameters, type GetReturnType, isStringLiteral, + isUnion, isTuple, tupleLength, - isUnion, } from "./index" -describe("Flinch Self-Test Suite", () => { - describe("Core Type Utilities", () => { - it("should validate Equal type utility", () => { - // Test Equal with identical types - expectTrue>() - expectTrue>() - expectTrue>() - - // Test Equal with different types - expectFalse>() - expectFalse>() - expectFalse>() - - // Test Equal with complex types - type ComplexType1 = { a: string; b: number; c?: boolean } - type ComplexType2 = { a: string; b: number; c?: boolean } - type ComplexType3 = { a: string; b: number; c: boolean } - - expectTrue>() - expectFalse>() - }) - - it("should validate NotEqual type utility", () => { - expectTrue>() - expectTrue>() - expectFalse>() - expectFalse>() - }) - - it("should validate Extends type utility", () => { - expectTrue>() - expectTrue>() - expectTrue>() - expectTrue>() - - expectFalse>() - expectFalse>() - expectFalse>() - }) - - it("should validate NotExtends type utility", () => { - expectTrue>() - expectTrue>() - expectFalse>() - expectFalse>() - }) - }) - - describe("Special Type Detection", () => { - it("should validate IsAny type utility", () => { - expectTrue>() - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - - it("should validate IsNever type utility", () => { - expectTrue>() - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - - // Test with conditional types that resolve to never - type NeverType = string & number - expectTrue>() - }) - - it("should validate IsUnknown type utility", () => { - expectTrue>() - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - }) - - describe("Structural Type Detection", () => { - it("should validate IsFunction type utility", () => { - expectTrue void>>() - expectTrue number>>() - expectTrue any>>() - expectTrue>() - - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - - it("should validate IsArray type utility", () => { - expectTrue>() - expectTrue>() - expectTrue>() - expectTrue>() - expectTrue>>() - - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - - it("should validate IsObject type utility", () => { - expectTrue>() - expectTrue>>() - expectTrue>() - expectTrue>() - - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - }) - - describe("Property Analysis", () => { - interface TestInterface { - required: string - optional?: number - readonly readonlyProp: boolean - normalProp: string +// Core Type Utilities Tests + +// Equal type utility validation +// Test Equal with identical types +expectTrue>() +expectTrue>() +expectTrue>() + +// Test Equal with different types +expectFalse>() +expectFalse>() +expectFalse>() + +// Test Equal with complex types +type ComplexType1 = { a: string; b: number; c?: boolean } +type ComplexType2 = { a: string; b: number; c?: boolean } +type ComplexType3 = { a: string; b: number; c: boolean } + +expectTrue>() +expectFalse>() + +// NotEqual type utility validation +expectTrue>() +expectTrue>() +expectFalse>() +expectFalse>() + +// Extends type utility validation +expectTrue>() +expectTrue>() +expectTrue>() +expectTrue>() + +expectFalse>() +expectFalse>() +expectFalse>() + +// NotExtends type utility validation +expectTrue>() +expectTrue>() +expectFalse>() +expectFalse>() + +// Special Type Detection Tests + +// IsAny type utility validation +expectTrue>() +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() + +// IsNever type utility validation +expectTrue>() +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() + +// Test with conditional types that resolve to never +type NeverType = string & number +expectTrue>() + +// IsUnknown type utility validation +expectTrue>() +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() + +// Structural Type Detection Tests + +// IsFunction type utility validation +expectTrue void>>() +expectTrue number>>() +expectTrue any>>() +expectTrue>() + +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() + +// IsArray type utility validation +expectTrue>() +expectTrue>() +expectTrue>() +expectTrue>() +expectTrue>>() + +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() + +// IsObject type utility validation +expectTrue>() +expectTrue>>() +expectTrue>() +expectTrue>() + +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() +expectFalse>() + +// Property Analysis Tests + +interface TestInterface { + required: string + optional?: number + readonly readonlyProp: boolean + normalProp: string +} + +// HasProperty type utility validation +expectTrue>() +expectTrue>() +expectTrue>() +expectTrue>() + +expectFalse>() +expectFalse>() + +// IsOptional type utility validation +expectTrue>() +expectFalse>() +expectFalse>() +expectFalse>() + +// IsReadonly type utility validation +expectTrue>() +expectFalse>() +expectFalse>() +expectFalse>() + +// Function Type Utilities Tests + +type TestFunction = (a: string, b: number, c?: boolean) => Promise +type VoidFunction = () => void + +// GetParameters type utility validation +expectTrue, [string, number, boolean?]>>() +expectTrue, []>>() + +// Test with built-in function types +expectTrue, [string, number?]>>() + +// GetReturnType type utility validation +expectTrue, Promise>>() +expectTrue, void>>() +expectTrue number>, number>>() + +// Advanced Type Utilities Tests + +// IsUnion type utility validation +expectTrue>() +expectTrue>() +expectTrue>() // boolean is true | false + +expectFalse>() +expectFalse>() +expectFalse>() + +// IsTuple type utility validation +expectTrue>() +expectTrue>() +expectTrue>() +expectTrue>() + +expectFalse>() +expectFalse>() +expectFalse>>() + +// TupleLength type utility validation +expectTrue, 2>>() +expectTrue, 3>>() +expectTrue, 0>>() +expectTrue, 1>>() + +// Head and Tail type utilities validation +type TestTuple = [string, number, boolean] + +expectTrue, string>>() +expectTrue, [number, boolean]>>() +expectTrue, number>>() +expectTrue, []>>() + +// Literal type utilities validation +expectTrue>() +expectTrue>() +expectFalse>() + +expectTrue>() +expectTrue>() +expectTrue>() +expectFalse>() + +// Type Assertion Functions Tests + +// Flinch functions validation - these should compile without errors if the type assertions are correct +flinch.equal() +flinch.notEqual() +flinch.extends() +flinch.notExtends() + +flinch.isFunction<() => void>() +flinch.isArray() +flinch.isObject<{ a: string }>() + +interface TestType { + prop: string + optional?: number + readonly readonlyProp: boolean +} + +flinch.hasProperty() +flinch.isOptional() +flinch.isReadonly() + +// Complex Type Scenarios Tests + +// Deeply nested generic types +interface ApiResponse { + data: T + meta: { + status: number + message?: string + } +} + +interface User { + id: number + profile: { + name: string + settings: { + theme: "light" | "dark" + notifications: boolean } - - it("should validate HasProperty type utility", () => { - expectTrue>() - expectTrue>() - expectTrue>() - expectTrue>() - - expectFalse>() - expectFalse>() - }) - - it("should validate IsOptional type utility", () => { - expectTrue>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - - it("should validate IsReadonly type utility", () => { - expectTrue>() - expectFalse>() - expectFalse>() - expectFalse>() - }) - }) - - describe("Function Type Utilities", () => { - type TestFunction = (a: string, b: number, c?: boolean) => Promise - type GenericFunction = (x: T) => T - type VoidFunction = () => void - - it("should validate GetParameters type utility", () => { - expectTrue< - Equal, [string, number, boolean?]> - >() - expectTrue, []>>() - - // Test with built-in function types - expectTrue, [string, number?]>>() - }) - - it("should validate GetReturnType type utility", () => { - expectTrue, Promise>>() - expectTrue, void>>() - expectTrue number>, number>>() - }) - }) - - describe("Advanced Type Utilities", () => { - it("should validate IsUnion type utility", () => { - expectTrue>() - expectTrue>() - expectTrue>() // boolean is true | false - - expectFalse>() - expectFalse>() - expectFalse>() - }) - - it("should validate IsTuple type utility", () => { - expectTrue>() - expectTrue>() - expectTrue>() - expectTrue>() - - expectFalse>() - expectFalse>() - expectFalse>>() - }) - - it("should validate TupleLength type utility", () => { - expectTrue, 2>>() - expectTrue, 3>>() - expectTrue, 0>>() - expectTrue, 1>>() - }) - - it("should validate Head and Tail type utilities", () => { - type TestTuple = [string, number, boolean] - - expectTrue, string>>() - expectTrue, [number, boolean]>>() - expectTrue, number>>() - expectTrue, []>>() - }) - - it("should validate literal type utilities", () => { - expectTrue>() - expectTrue>() - expectFalse>() - - expectTrue>() - expectTrue>() - expectTrue>() - expectFalse>() - }) - }) - - describe("Type Assertion Functions", () => { - it("should validate flinch functions work correctly", () => { - // These should compile without errors if the type assertions are correct - flinch.equal() - flinch.notEqual() - flinch.extends() - flinch.notExtends() - - flinch.isFunction<() => void>() - flinch.isArray() - flinch.isObject<{ a: string }>() - - interface TestType { - prop: string - optional?: number - readonly readonlyProp: boolean - } - - flinch.hasProperty() - flinch.isOptional() - flinch.isReadonly() - }) - }) - - describe("Complex Type Scenarios", () => { - it("should handle deeply nested generic types", () => { - interface ApiResponse { - data: T - meta: { - status: number - message?: string - } - } - - interface User { - id: number - profile: { - name: string - settings: { - theme: "light" | "dark" - notifications: boolean - } - } - } - - type UserApiResponse = ApiResponse - - // Test nested property access - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - - // Test deep type equality - flinch.equal< - UserApiResponse["data"]["profile"]["settings"]["theme"], - "light" | "dark" - >() - flinch.equal() - - // Test optional properties at different levels - flinch.isOptional() - }) - - it("should handle conditional and mapped types", () => { - // Conditional type - type NonNullable = T extends null | undefined ? never : T - - flinch.equal, string>() - flinch.equal, string>() - flinch.isNever>() - - // Mapped type - type Partial = { - [P in keyof T]?: T[P] - } - - interface Original { - a: string - b: number - } - - type PartialOriginal = Partial - - flinch.isOptional() - flinch.isOptional() - flinch.equal() - }) - - it("should handle template literal types", () => { - type EventName = `on${Capitalize}` - type ClickEvent = EventName<"click"> - type HoverEvent = EventName<"hover"> - - flinch.equal() - flinch.equal() - - // Test that these are string literals - isStringLiteral() - isStringLiteral() - }) - }) - - describe("Edge Cases and Error Conditions", () => { - it("should handle empty types correctly", () => { - type EmptyObject = {} - type EmptyTuple = [] - type EmptyUnion = never - - flinch.isObject() - isTuple() - tupleLength() - flinch.isNever() - }) - - it("should handle recursive types", () => { - interface TreeNode { - value: string - children?: TreeNode[] - } - - flinch.hasProperty() - flinch.hasProperty() - flinch.isOptional() - - // Test that children is an array of TreeNode - type ChildrenType = NonNullable - flinch.isArray() - }) - - it("should handle intersection types", () => { - interface A { - a: string - } - - interface B { - b: number - } - - type AB = A & B - - flinch.hasProperty() - flinch.hasProperty() - flinch.equal() - flinch.equal() - - // Test that AB extends both A and B - flinch.extends() - flinch.extends() - }) - }) -}) - -// Test that flinch methods properly fail with invalid types -describe("Flinch Method Failure Tests", () => { - interface TestInterface { - existingProp: string - optionalProp?: number - readonly readonlyProp: boolean } +} + +type UserApiResponse = ApiResponse + +// Test nested property access +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() + +// Test deep type equality +flinch.equal< + UserApiResponse["data"]["profile"]["settings"]["theme"], + "light" | "dark" +>() +flinch.equal() + +// Test optional properties at different levels +flinch.isOptional() + +// Conditional and mapped types +// Conditional type +type NonNullable = T extends null | undefined ? never : T + +flinch.equal, string>() +flinch.equal, string>() +flinch.isNever>() + +// Mapped type +type Partial = { + [P in keyof T]?: T[P] +} + +interface Original { + a: string + b: number +} + +type PartialOriginal = Partial + +flinch.isOptional() +flinch.isOptional() +flinch.equal() + +// Template literal types +type EventName = `on${Capitalize}` +type ClickEvent = EventName<"click"> +type HoverEvent = EventName<"hover"> + +flinch.equal() +flinch.equal() + +// Test that these are string literals +isStringLiteral() +isStringLiteral() - it("should cause compilation errors for invalid type assertions", () => { - // These should all cause TypeScript compilation errors: +// Edge Cases and Error Conditions Tests - // @ts-expect-error - string does not equal number - flinch.equal() +// Empty types handling +type EmptyObject = {} +type EmptyTuple = [] +type EmptyUnion = never - // @ts-expect-error - string equals string - flinch.notEqual() +flinch.isObject() +isTuple() +tupleLength() +flinch.isNever() - // @ts-expect-error - string does not extend number - flinch.extends() +// Recursive types handling +interface TreeNode { + value: string + children?: TreeNode[] +} - // @ts-expect-error - string extends string | number - flinch.notExtends() +flinch.hasProperty() +flinch.hasProperty() +flinch.isOptional() - // @ts-expect-error - string is not any - flinch.isAny() +// Test that children is an array of TreeNode +type ChildrenType = NonNullable +flinch.isArray() - // @ts-expect-error - string is not never - flinch.isNever() +// Intersection types handling +interface A { + a: string +} - // @ts-expect-error - string is not unknown - flinch.isUnknown() +interface B { + b: number +} - // @ts-expect-error - string is not a function - flinch.isFunction() +type AB = A & B - // @ts-expect-error - string is not an array - flinch.isArray() +flinch.hasProperty() +flinch.hasProperty() +flinch.equal() +flinch.equal() - // @ts-expect-error - string is not an object - flinch.isObject() +// Test that AB extends both A and B +flinch.extends() +flinch.extends() - // @ts-expect-error - arrays are not objects in our definition - flinch.isObject() +// Flinch Method Failure Tests +// These tests verify that flinch methods properly fail with invalid types - // @ts-expect-error - functions are not objects in our definition - flinch.isObject<() => void>() +interface TestInterface { + existingProp: string + optionalProp?: number + readonly readonlyProp: boolean +} - // @ts-expect-error - property doesn't exist - flinch.hasProperty() +// These should all cause TypeScript compilation errors: - // @ts-expect-error - existingProp is required, not optional - flinch.isOptional() +// @ts-expect-error - string does not equal number +flinch.equal() - // @ts-expect-error - existingProp is not readonly - flinch.isReadonly() - }) -}) +// @ts-expect-error - string equals string +flinch.notEqual() +// @ts-expect-error - string does not extend number +flinch.extends() + +// @ts-expect-error - string extends string | number +flinch.notExtends() + +// @ts-expect-error - string is not any +flinch.isAny() + +// @ts-expect-error - string is not never +flinch.isNever() + +// @ts-expect-error - string is not unknown +flinch.isUnknown() + +// @ts-expect-error - string is not a function +flinch.isFunction() + +// @ts-expect-error - string is not an array +flinch.isArray() + +// @ts-expect-error - string is not an object +flinch.isObject() + +// @ts-expect-error - arrays are not objects in our definition +flinch.isObject() + +// @ts-expect-error - functions are not objects in our definition +flinch.isObject<() => void>() + +// @ts-expect-error - property doesn't exist +flinch.hasProperty() + +// @ts-expect-error - existingProp is required, not optional +flinch.isOptional() + +// @ts-expect-error - existingProp is not readonly +flinch.isReadonly() + +// Flinch API Validation Tests // Test that Flinch can validate its own API -describe("Flinch API Validation", () => { - it("should validate flinch API structure", () => { - flinch.isObject() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - flinch.hasProperty() - - // All methods should be functions - flinch.isFunction() - flinch.isFunction() - flinch.isFunction() - flinch.isFunction() - }) - - it("should validate advanced type assertions", () => { - // All methods should be functions - flinch.isFunction() - flinch.isFunction() - flinch.isFunction() - flinch.isFunction() - }) -}) + +// Validate flinch API structure +flinch.isObject() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() +flinch.hasProperty() + +// All methods should be functions +flinch.isFunction() +flinch.isFunction() +flinch.isFunction() +flinch.isFunction() + +// Validate advanced type assertions +flinch.isFunction() +flinch.isFunction() +flinch.isFunction() +flinch.isFunction() diff --git a/test/types.ts b/test/types.ts index 3b90a43..361b27c 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,4 +1,10 @@ -import { describe, it } from "node:test" +/** + * Flickr SDK Type Tests + * + * This file contains compile-time type assertions to verify the correctness + * of the Flickr SDK type definitions using the Flinch type testing framework. + */ + import assert, { Equal, Extends, IsTuple, expectTrue } from "@flickr/flinch" import type { API, @@ -14,128 +20,104 @@ import type { FlickrTestEchoParams, } from "flickr-sdk" -describe("flickr-sdk type tests", () => { - describe("Auth Interface", () => { - it("should have correct method signature", () => { - // Verify Auth interface structure - assert.hasProperty() - assert.isFunction() - - // Test the sign method parameters and return type - type SignMethod = Auth["sign"] - assert.isFunction() - - // The sign method should return a Promise - type SignReturnType = ReturnType - assert.extends>() - }) - }) - - describe("Transport Interface", () => { - it("should have get and post methods", () => { - assert.hasProperty() - assert.hasProperty() - - assert.isFunction() - assert.isFunction() - }) - - it("should have correct method signatures", () => { - // Test get method - type GetMethod = Transport["get"] - type GetReturnType = ReturnType - assert.extends>() - - // Test post method - type PostMethod = Transport["post"] - type PostReturnType = ReturnType - assert.extends>() - }) - }) - - describe("Parser Interface", () => { - it("should have parse method with correct signature", () => { - assert.hasProperty() - assert.isFunction() - - type ParseMethod = Parser["parse"] - type ParseReturnType = ReturnType - assert.extends>() - }) - }) - - describe("Parameter Types", () => { - it("should verify GET and POST extend Params", () => { - assert.extends() - assert.extends() - }) - }) - - describe("Integration Type Tests", () => { - it("should verify interface compatibility", () => { - // Test that implementations would satisfy the interfaces - interface MockAuth extends Auth {} - interface MockTransport extends Transport {} - interface MockParser extends Parser {} - - assert.extends() - assert.extends() - assert.extends() - }) - }) - - describe("API", function () { - it("has flickr.people.getInfo", () => { - assert.hasProperty() - - expectTrue>() - expectTrue< - Equal - >() - }) - - it("has flickr.test.echo", () => { - assert.hasProperty() - - expectTrue>() - expectTrue>() - }) - }) - - describe("APIKeyConfig", () => { - it("accepts a string", () => { - expectTrue>() - expectTrue string, APIKeyAuthConfig>>() - expectTrue Promise, APIKeyAuthConfig>>() - }) - }) - - describe("OAuthConfig", () => { - it("accepts consumer key/secret and oauth token/secret", () => { - expectTrue< - Extends< - { - consumerKey: string - consumerSecret: string - oauthToken: string - oauthTokenSecret: string - }, - OAuthConfig - > - >() - }) - it("can specify false for oauth token/secret", () => { - expectTrue< - Extends< - { - consumerKey: string - consumerSecret: string - oauthToken: false - oauthTokenSecret: false - }, - OAuthConfig - > - >() - }) - }) -}) +// Auth Interface Tests +// Verify Auth interface structure +assert.hasProperty() +assert.isFunction() + +// Test the sign method parameters and return type +type SignMethod = Auth["sign"] +assert.isFunction() + +// The sign method should return a Promise +type SignReturnType = ReturnType +assert.extends>() + +// Transport Interface Tests +// Should have get and post methods +assert.hasProperty() +assert.hasProperty() + +assert.isFunction() +assert.isFunction() + +// Should have correct method signatures +// Test get method +type GetMethod = Transport["get"] +type GetReturnType = ReturnType +assert.extends>() + +// Test post method +type PostMethod = Transport["post"] +type PostReturnType = ReturnType +assert.extends>() + +// Parser Interface Tests +// Should have parse method with correct signature +assert.hasProperty() +assert.isFunction() + +type ParseMethod = Parser["parse"] +type ParseReturnType = ReturnType +assert.extends>() + +// Parameter Types Tests +// Should verify GET and POST extend Params +assert.extends() +assert.extends() + +// Integration Type Tests +// Test that implementations would satisfy the interfaces +interface MockAuth extends Auth {} +interface MockTransport extends Transport {} +interface MockParser extends Parser {} + +assert.extends() +assert.extends() +assert.extends() + +// API Tests +// Should have flickr.people.getInfo +assert.hasProperty() + +expectTrue>() +expectTrue>() + +// Should have flickr.test.echo +assert.hasProperty() + +expectTrue>() +expectTrue>() + +// APIKeyConfig Tests +// Should accept a string +expectTrue>() +expectTrue string, APIKeyAuthConfig>>() +expectTrue Promise, APIKeyAuthConfig>>() + +// OAuthConfig Tests +// Should accept consumer key/secret and oauth token/secret +expectTrue< + Extends< + { + consumerKey: string + consumerSecret: string + oauthToken: string + oauthTokenSecret: string + }, + OAuthConfig + > +>() + +// Should allow false for oauth token/secret +expectTrue< + Extends< + { + consumerKey: string + consumerSecret: string + oauthToken: false + oauthTokenSecret: false + }, + OAuthConfig + > +>() From 77f4b08302e05a2adf8e57f1bb96692526dbe5ab Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Fri, 18 Jul 2025 16:26:53 -0400 Subject: [PATCH 4/4] simplify types / flinch tests --- packages/flinch/src/index.ts | 18 +++++++----------- packages/flinch/src/test.ts | 8 ++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/flinch/src/index.ts b/packages/flinch/src/index.ts index d6cd785..cf26bd4 100644 --- a/packages/flinch/src/index.ts +++ b/packages/flinch/src/index.ts @@ -186,7 +186,7 @@ const flinch = { * Usage: expectTrue>() // OK * expectTrue>() // TypeScript error */ -export function expectTrue(): void { +export function expectTrue() { // Compile-time only assertion } @@ -196,7 +196,7 @@ export function expectTrue(): void { * Usage: expectFalse>() // OK * expectFalse>() // TypeScript error */ -export function expectFalse(): void { +export function expectFalse() { // Compile-time only assertion } @@ -207,14 +207,14 @@ export function expectFalse(): void { /** * Assert that a type is a union type */ -export function isUnion(): IsUnion extends true ? void : never { +export function isUnion() { return undefined as IsUnion extends true ? void : never } /** * Assert that a type is a tuple (fixed-length array) */ -export function isTuple(): IsTuple extends true ? void : never { +export function isTuple() { return undefined as IsTuple extends true ? void : never } @@ -224,25 +224,21 @@ export function isTuple(): IsTuple extends true ? void : never { export function tupleLength< T extends readonly any[], N extends number, ->(): TupleLength extends N ? void : never { +>() { return undefined as TupleLength extends N ? void : never } /** * Assert that a type is a string literal */ -export function isStringLiteral(): IsStringLiteral extends true - ? void - : never { +export function isStringLiteral() { return undefined as IsStringLiteral extends true ? void : never } /** * Assert that a type is a number literal */ -export function isNumberLiteral(): IsNumberLiteral extends true - ? void - : never { +export function isNumberLiteral() { return undefined as IsNumberLiteral extends true ? void : never } diff --git a/packages/flinch/src/test.ts b/packages/flinch/src/test.ts index ffad8cd..5c0ee3c 100644 --- a/packages/flinch/src/test.ts +++ b/packages/flinch/src/test.ts @@ -35,6 +35,7 @@ import flinch, { isUnion, isTuple, tupleLength, + isNumberLiteral, } from "./index" // Core Type Utilities Tests @@ -439,6 +440,12 @@ flinch.isOptional() // @ts-expect-error - existingProp is not readonly flinch.isReadonly() +// @ts-expect-error - false is not true +expectTrue() + +// @ts-expect-error - true is not false +expectFalse() + // Flinch API Validation Tests // Test that Flinch can validate its own API @@ -469,3 +476,4 @@ flinch.isFunction() flinch.isFunction() flinch.isFunction() flinch.isFunction() +flinch.isFunction()