Skip to content

Commit

Permalink
add visibility docs, allow spreading object domain
Browse files Browse the repository at this point in the history
  • Loading branch information
ssalbdivad committed Jan 3, 2025
1 parent a2a3a76 commit 3eaebeb
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 2 deletions.
57 changes: 56 additions & 1 deletion ark/docs/content/docs/scopes/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,62 @@ Luckily, despite its appearance, the type otherwise behaves as you'd expect- Typ

### visibility

🚧 Coming soon ™️🚧
Intermediate aliases can be useful for composing Scoped definitions from aliases. Sometimes, you may not want to expose those aliases externally as `Type`s when your `Scope` is `export`ed.

This can be done using _private_ aliases:

```ts
const shapeScope = scope({
// aliases with a "#" prefix are treated as private
"#baseShapeProps": {
perimeter: "number",
area: "number"
},
ellipse: {
// when referencing a private alias, the "#" should not be included
"...": "baseShapeProps",
radii: ["number", "number"]
},
rectangle: {
"...": "baseShapeProps",
width: "number",
height: "number"
}
})

// private aliases can be referenced from any scoped definition,
// even outside the original scope
const partialShape = shapeScope.type("Partial<baseShapeProps>")

// when the scope is exported to a Module, they will not be included
// hover to see the Scope's exports
const shapeModule = shapeScope.export()
```

#### `import()`

Private aliases are especially useful for building scopes without polluting them with every alias you might want to reference internally. To facilitate this, Scopes have an `import()` method that behaves identically to `export()` but converts all exported aliases to `private`.

```ts
const utilityScope = scope({
"withId<o extends object>": {
"...": "o",
id: "string"
}
})

const userModule = type.module({
// because we use `import()` here, we can reference our utilities
// internally, but they will not be included in `userModule`.
// if we used `export()` instead, `withId` could be accessed on `userModule`.
...utilityScope.import(),
payload: {
name: "string",
age: "number"
},
db: "withId<payload>"
})
```

### submodules

Expand Down
68 changes: 68 additions & 0 deletions ark/type/__tests__/imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ contextualize(() => {

const exports = imported.export()

attest(Object.keys(exports)).equals(["a"])
attest(exports.a.expression).snap('"no" | "yes" | 3 | 60 | true')

attest<Module<{ a: 3 | 60 | "no" | "yes" | true }>>(exports)
})

Expand Down Expand Up @@ -83,6 +86,71 @@ contextualize(() => {
}
)

it("docs example", () => {
const shapeScope = scope({
// aliases with a "#" prefix are treated as private
"#baseShapeProps": {
perimeter: "number",
area: "number"
},
ellipse: {
// when referencing a private alias, the "#" should not be included
"...": "baseShapeProps",
radii: ["number", "number"]
},
rectangle: {
"...": "baseShapeProps",
width: "number",
height: "number"
}
})

// private aliases can be referenced from any scoped definition,
// even outside the original scope
const partialShape = shapeScope.type("Partial<baseShapeProps>")

attest<{
perimeter?: number
area?: number
}>(partialShape.t)
attest<typeof shapeScope>(partialShape.$)

attest(partialShape.expression).snap(
"{ area?: number, perimeter?: number }"
)

// when the scope is exported to a Module, they will not be included
// hover to see the Scope's exports
const shapeModule = shapeScope.export()

attest(Object.keys(shapeModule)).equals(["ellipse", "rectangle"])
attest(shapeModule).type.toString.snap()
})

it("docs import example", () => {
const utilityScope = scope({
"withId<o extends object>": {
"...": "o",
id: "string"
}
})

const userModule = type.module({
// because we use `import()` here, we can reference our utilities
// internally, but they will not be included in `userModule`.
// if we used `export()` instead, `withId` could be accessed on `userModule`.
...utilityScope.import(),
payload: {
name: "string",
age: "number"
},
db: "withId<payload>"
})

attest(Object.keys(userModule)).equals(["payload", "db"])
attest(userModule).type.toString.snap()
})

it("binds destructured exports", () => {
const types = scope({
foo: "1",
Expand Down
33 changes: 33 additions & 0 deletions ark/type/__tests__/objects/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,37 @@ contextualize(() => {
]
})
})

it("object keyword treated as empty", () => {
const t = type({
"...": "object",
foo: "string"
})

attest<{
foo: string
}>(t.t)
attest(t.expression).snap()
})

it("narrowed object keyword treated as empty", () => {
const t = type({
"...": type.object.narrow(() => true),
foo: "string"
})

attest<{
foo: string
}>(t.t)
attest(t.expression).snap("{ foo: string }")
})

it("errors on proto node", () => {
attest(() =>
type({
"...": "Date",
foo: "string"
})
).throws(writeInvalidSpreadTypeMessage("Date"))
})
})
9 changes: 8 additions & 1 deletion ark/type/parser/objectLiteral.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
intrinsic,
normalizeIndex,
type BaseParseContext,
type BaseRoot,
Expand Down Expand Up @@ -57,7 +58,13 @@ export const parseObjectLiteral = (
if (!isEmptyObject(structure))
return throwParseError(nonLeadingSpreadError)
const operand = ctx.$.parseOwnDefinitionFormat(v, ctx)
if (!operand.hasKind("intersection") || !operand.structure) {
// treat object domain as empty for spreading (useful for generic constraints)
if (operand.equals(intrinsic.object)) continue
if (
!operand.hasKind("intersection") ||
// still error on attempts to spread proto nodes like ...Date
!operand.basis?.equals(intrinsic.object)
) {
return throwParseError(
writeInvalidSpreadTypeMessage(operand.expression)
)
Expand Down

0 comments on commit 3eaebeb

Please sign in to comment.