diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index ada457b50a..00f4a0a7f0 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -3,7 +3,7 @@ description: Install dependencies and perform setup for https://github.com/arkty inputs: node: - default: lts/* + default: 18 runs: using: composite diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b18891f55a..9de02e8c4d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -29,14 +29,12 @@ jobs: timeout-minutes: 20 strategy: matrix: - node: [lts/*] + # https://github.com/arktypeio/arktype/issues/738 + node: [16, 18] os: [windows-latest, macos-latest] include: - os: ubuntu-latest - node: lts/-1 - # https://github.com/arktypeio/arktype/issues/738 - # - os: ubuntu-latest - # node: latest + node: 16 fail-fast: false runs-on: ${{ matrix.os }} diff --git a/dev/configs/.changeset/fast-onions-boil.md b/dev/configs/.changeset/fast-onions-boil.md new file mode 100644 index 0000000000..78282aa6b2 --- /dev/null +++ b/dev/configs/.changeset/fast-onions-boil.md @@ -0,0 +1,5 @@ +--- +"arktype": patch +--- + +Allow instanceof abstract classes, avoid treating some function props as morphs diff --git a/dev/test/instanceof.test.ts b/dev/test/instanceof.test.ts index ddd076bb89..df60b165f0 100644 --- a/dev/test/instanceof.test.ts +++ b/dev/test/instanceof.test.ts @@ -24,6 +24,18 @@ describe("instanceof", () => { "Must be an instance of TypeError (was Error)" ) }) + it("abstract", () => { + abstract class Base { + abstract foo: string + } + class Sub extends Base { + foo = "" + } + const t = type(["instanceof", Base]) + attest(t.infer).typed as Base + const sub = new Sub() + attest(t(sub).data).equals(sub) + }) it("builtins not evaluated", () => { const t = type(["instanceof", Date]) attest(t.infer).types.toString("Date") diff --git a/dev/test/morph.test.ts b/dev/test/morph.test.ts index 94a842b8a1..ff6100028b 100644 --- a/dev/test/morph.test.ts +++ b/dev/test/morph.test.ts @@ -1,13 +1,14 @@ import { describe, it } from "mocha" -import { ark, intersection, morph, scope, type, union } from "../../src/main.js" import type { Problem, Type } from "../../src/main.js" +import { ark, intersection, morph, scope, type, union } from "../../src/main.js" +import type { Out } from "../../src/parse/ast/morph.js" import { writeUndiscriminatableMorphUnionMessage } from "../../src/parse/ast/union.js" import { attest } from "../attest/main.js" describe("morph", () => { it("base", () => { const t = type(["boolean", "|>", (data) => `${data}`]) - attest(t).typed as Type<(In: boolean) => string> + attest(t).typed as Type<(In: boolean) => Out> attest(t.infer).typed as string attest(t.node).snap({ boolean: { rules: {}, morph: "(function)" } }) const result = t(true) @@ -19,7 +20,7 @@ describe("morph", () => { }) it("endomorph", () => { const t = type(["boolean", "|>", (data) => !data]) - attest(t).typed as Type<(In: boolean) => boolean> + attest(t).typed as Type<(In: boolean) => Out> const result = t(true) if (result.problems) { return result.problems.throw() @@ -28,7 +29,7 @@ describe("morph", () => { }) it("from type", () => { const t = type(["string>5", "|>", ark.parsedDate]) - attest(t).typed as Type<(In: string) => Date> + attest(t).typed as Type<(In: string) => Out> attest(t("5/21/1993").data?.getDate()).equals(21) attest(t("foobar").problems?.summary).snap( "Must be a valid date (was 'foobar')" @@ -40,7 +41,7 @@ describe("morph", () => { "|>", (n, problems) => (n === 0 ? problems.mustBe("non-zero") : 100 / n) ]) - attest(divide100By).typed as Type<(In: number) => number> + attest(divide100By).typed as Type<(In: number) => Out> attest(divide100By(5).data).equals(20) attest(divide100By(0).problems?.summary).snap( "Must be non-zero (was 0)" @@ -67,7 +68,7 @@ describe("morph", () => { it("at path", () => { const t = type({ a: ["string", "|>", (data) => data.length] }) attest(t).typed as Type<{ - a: (In: string) => number + a: (In: string) => Out }> const result = t({ a: "four" }) if (result.problems) { @@ -82,7 +83,9 @@ describe("morph", () => { lengthOfString: ["string", "|>", (data) => data.length], mapToLengths: "lengthOfString[]" }).compile() - attest(types.mapToLengths).typed as Type<((In: string) => number)[]> + attest(types.mapToLengths).typed as Type< + ((In: string) => Out)[] + > const result = types.mapToLengths(["1", "22", "333"]) if (result.problems) { return result.problems.throw() @@ -91,7 +94,7 @@ describe("morph", () => { }) it("object inference", () => { const t = type([{ a: "string" }, "|>", (data) => `${data}`]) - attest(t).typed as Type<(In: { a: string }) => string> + attest(t).typed as Type<(In: { a: string }) => Out> }) it("intersection", () => { const $ = scope({ @@ -101,7 +104,7 @@ describe("morph", () => { bAndA: () => $.type("b&a") }) const types = $.compile() - attest(types.aAndB).typed as Type<(In: 3.14) => string> + attest(types.aAndB).typed as Type<(In: 3.14) => Out> attest(types.aAndB.node).snap({ number: { rules: { value: 3.14 }, morph: "(function)" } }) @@ -115,7 +118,7 @@ describe("morph", () => { c: "a&b" }) const types = $.compile() - attest(types.c).typed as Type<(In: { a: 1; b: 2 }) => string> + attest(types.c).typed as Type<(In: { a: 1; b: 2 }) => Out> attest(types.c.node).snap({ object: { rules: { @@ -135,7 +138,9 @@ describe("morph", () => { aOrB: "a|b", bOrA: "b|a" }).compile() - attest(types.aOrB).typed as Type string)> + attest(types.aOrB).typed as Type< + boolean | ((In: number) => Out) + > attest(types.aOrB.node).snap({ number: { rules: {}, morph: "(function)" }, boolean: true @@ -150,7 +155,7 @@ describe("morph", () => { c: "a&b" }).compile() attest(types.c).typed as Type<{ - a: (In: 1) => number + a: (In: 1) => Out }> attest(types.c.node).snap({ object: { @@ -168,7 +173,7 @@ describe("morph", () => { }).compile() attest(types.c).typed as Type< | { - a: (In: number) => string + a: (In: number) => Out } | { a: (...args: any[]) => unknown @@ -200,7 +205,7 @@ describe("morph", () => { b: () => $.morph("a", (n) => n === 0) }) const types = $.compile() - attest(types.b).typed as Type<(In: string) => boolean> + attest(types.b).typed as Type<(In: string) => Out> attest(types.b.node).snap({ string: { rules: {}, morph: ["(function)", "(function)"] } }) @@ -211,7 +216,7 @@ describe("morph", () => { b: () => $.morph({ a: "a" }, ({ a }) => a === 0) }) const types = $.compile() - attest(types.b).typed as Type<(In: { a: string }) => boolean> + attest(types.b).typed as Type<(In: { a: string }) => Out> attest(types.b.node).snap({ object: { rules: { props: { a: "a" } }, morph: "(function)" } }) @@ -224,7 +229,7 @@ describe("morph", () => { "|>", ({ a }) => a === 0 ]) - attest(t).typed as Type<(In: { a: string }) => boolean> + attest(t).typed as Type<(In: { a: string }) => Out> attest(t.node).snap({ object: { rules: { @@ -241,7 +246,9 @@ describe("morph", () => { c: () => $.type("a|b") }) const types = $.compile() - attest(types.c).typed as Type<[boolean] | ((In: [string]) => string[])> + attest(types.c).typed as Type< + [boolean] | ((In: [string]) => Out) + > attest(types.c.node).snap({ object: [ { @@ -336,7 +343,7 @@ describe("morph", () => { const t = $.type("a|b") attest(t).typed as Type< | { - a: (In: string) => string + a: (In: string) => Out } | { a: boolean @@ -391,7 +398,7 @@ describe("morph", () => { return result } ]) - attest(parsedInt).typed as Type<(In: string) => number> + attest(parsedInt).typed as Type<(In: string) => Out> attest(parsedInt("5").data).snap(5) attest(parsedInt("five").problems?.summary).snap( "Must be an integer string (was 'five')" @@ -399,7 +406,9 @@ describe("morph", () => { }) it("nullable return", () => { const toNullableNumber = type(["string", "|>", (s) => s.length || null]) - attest(toNullableNumber).typed as Type<(In: string) => number | null> + attest(toNullableNumber).typed as Type< + (In: string) => Out + > }) it("undefinable return", () => { const toUndefinableNumber = type([ @@ -408,7 +417,7 @@ describe("morph", () => { (s) => s.length || undefined ]) attest(toUndefinableNumber).typed as Type< - (In: string) => number | undefined + (In: string) => Out > }) it("null or undefined return", () => { @@ -419,7 +428,7 @@ describe("morph", () => { s.length === 0 ? undefined : s.length === 1 ? null : s.length ]) attest(toMaybeNumber).typed as Type< - (In: string) => number | null | undefined + (In: string) => Out > }) }) diff --git a/src/parse/ast/intersection.ts b/src/parse/ast/intersection.ts index 9b9650d74a..4e83cf6730 100644 --- a/src/parse/ast/intersection.ts +++ b/src/parse/ast/intersection.ts @@ -3,7 +3,7 @@ import { disjointDescriptionWriters } from "../../nodes/compose.js" import type { asConst, evaluate, isAny, List } from "../../utils/generics.js" import { objectKeysOf } from "../../utils/generics.js" import type { Path, pathToString } from "../../utils/paths.js" -import type { ParsedMorph } from "./morph.js" +import type { Out, ParsedMorph } from "./morph.js" export type inferIntersection = [l] extends [never] ? never @@ -16,9 +16,9 @@ export type inferIntersection = [l] extends [never] : l extends ParsedMorph ? r extends ParsedMorph ? never - : (In: evaluate) => lOut + : (In: evaluate) => Out : r extends ParsedMorph - ? (In: evaluate) => rOut + ? (In: evaluate) => Out : intersectObjects extends infer result ? result : never diff --git a/src/parse/ast/morph.ts b/src/parse/ast/morph.ts index b180d31a5e..5007a8b746 100644 --- a/src/parse/ast/morph.ts +++ b/src/parse/ast/morph.ts @@ -65,10 +65,14 @@ export type validateMorphTuple = readonly [ export type Morph = (In: i, problems: Problems) => o -export type ParsedMorph = (In: i) => o +export type Out = readonly ["|>", o] + +export type ParsedMorph = (In: i) => Out export type inferMorph = morph extends Morph - ? (In: asIn>) => inferMorphOut> + ? ( + In: asIn> + ) => Out>> : never type inferMorphOut = [out] extends [CheckResult] diff --git a/src/scopes/ark.ts b/src/scopes/ark.ts index a88cdf1e71..1ad44494f3 100644 --- a/src/scopes/ark.ts +++ b/src/scopes/ark.ts @@ -1,3 +1,4 @@ +import type { Out } from "../parse/ast/morph.js" import { jsObjects, jsObjectsScope } from "./jsObjects.js" import type { Space } from "./scope.js" import { rootScope, scope } from "./scope.js" @@ -60,10 +61,10 @@ export type PrecompiledDefaults = { email: string uuid: string semver: string - json: (In: string) => unknown - parsedNumber: (In: string) => number - parsedInteger: (In: string) => number - parsedDate: (In: string) => Date + json: (In: string) => Out + parsedNumber: (In: string) => Out + parsedInteger: (In: string) => Out + parsedDate: (In: string) => Out // jsObjects Function: (...args: any[]) => unknown Date: Date diff --git a/src/utils/generics.ts b/src/utils/generics.ts index 9b7743bded..e6b0e4a878 100644 --- a/src/utils/generics.ts +++ b/src/utils/generics.ts @@ -78,7 +78,9 @@ export const isKeyOf = ( obj: obj ): k is Extract => k in obj -export type constructor = new (...args: any[]) => instance +export type constructor = abstract new ( + ...args: any[] +) => instance export type instanceOf> = classType extends constructor ? Instance : never