Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
chore: Align isObject/isRecord usage
  • Loading branch information
jsantell committed May 5, 2025
commit 141289d4051fe93e725fa66c2ad9a1e46958bb48
2 changes: 1 addition & 1 deletion background-charm-service/src/space-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sleep } from "@commontools/utils";
import { sleep } from "@commontools/utils/sleep";
import { Cell } from "@commontools/runner";
import { type Cancel, useCancelGroup } from "@commontools/runner";
import {
Expand Down
6 changes: 3 additions & 3 deletions builder/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isObj } from "@commontools/utils";
import { Mutable } from "@commontools/utils/types";
import { isObject, Mutable } from "@commontools/utils/types";

export const ID: unique symbol = Symbol("ID, unique to the context");
export const ID_FIELD: unique symbol = Symbol(
Expand Down Expand Up @@ -166,7 +165,8 @@ export type Alias = {
};

export function isAlias(value: any): value is Alias {
return isObj(value) && isObj(value.$alias) &&
return isObject(value) && "$alias" in value && isObject(value.$alias) &&
"path" in value.$alias &&
Array.isArray(value.$alias.path);
}

Expand Down
10 changes: 5 additions & 5 deletions charm/src/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
recipeManager,
runtime,
} from "@commontools/runner";
import { isObj } from "@commontools/utils";
import { isObject } from "@commontools/utils/types";
import {
createJsonSchema,
JSONSchema,
Expand Down Expand Up @@ -215,7 +215,7 @@ export function scrub(data: any): any {
// If there are properties, remove $UI and $NAME and any streams
const scrubbed = Object.fromEntries(
Object.entries(data.schema.properties).filter(([key, value]) =>
!key.startsWith("$") && (!isObj(value) || !value.asStream)
!key.startsWith("$") && (!isObject(value) || !value.asStream)
),
);
console.log("scrubbed modified schema", scrubbed, data.schema);
Expand All @@ -227,7 +227,7 @@ export function scrub(data: any): any {
);
} else {
const value = data.asSchema().get();
if (isObj(value)) {
if (isObject(value)) {
// Generate a new schema for all properties except $UI and $NAME and streams
const scrubbed = {
type: "object",
Expand All @@ -248,7 +248,7 @@ export function scrub(data: any): any {
}
} else if (Array.isArray(data)) {
return data.map((value) => scrub(value));
} else if (isObj(data)) {
} else if (isObject(data)) {
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, scrub(value)]),
);
Expand All @@ -264,7 +264,7 @@ function turnCellsIntoAliases(data: any): any {
return { $alias: data.getAsCellLink() };
} else if (Array.isArray(data)) {
return data.map((value) => turnCellsIntoAliases(value));
} else if (isObj(data)) {
} else if (isObject(data)) {
return Object.fromEntries(
Object.entries(data).map((
[key, value],
Expand Down
4 changes: 2 additions & 2 deletions charm/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from "@commontools/runner";
import { storage } from "@commontools/runner";
import { type Session } from "@commontools/identity";
import { isObj } from "@commontools/utils";
import { isObject } from "@commontools/utils/types";

export function charmId(charm: Charm): string | undefined {
const id = getEntityId(charm);
Expand Down Expand Up @@ -343,7 +343,7 @@ export class CharmManager {
// If there is no result schema, create one from top level properties that omits UI, NAME
if (!resultSchema) {
const resultValue = charm.get();
if (isObj(resultValue)) {
if (isObject(resultValue)) {
resultSchema = {
type: "object",
properties: Object.fromEntries(
Expand Down
1 change: 1 addition & 0 deletions html/deno.json → html/deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@commontools/html",
"exports": "./src/index.ts",
"tasks": {
// JSDOM dependencies require env.
"test": "deno test --allow-env"
},
"imports": {
Expand Down
5 changes: 1 addition & 4 deletions html/src/path.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import * as logger from "./logger.ts";

export const isObject = (value: unknown): value is object => {
return typeof value === "object" && value !== null;
};
import { isObject } from "@commontools/utils/types";

/** A keypath is an array of property keys */
export type KeyPath = Array<PropertyKey>;
Expand Down
4 changes: 2 additions & 2 deletions html/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useCancelGroup,
} from "@commontools/runner";
import { JSONSchema } from "@commontools/builder";
import { isObj } from "@commontools/utils";
import { isObject } from "@commontools/utils/types";
import * as logger from "./logger.ts";

const vdomSchema: JSONSchema = {
Expand Down Expand Up @@ -271,7 +271,7 @@ const sanitizeScripts = (node: VNode): VNode | null => {
if (node.name === "script") {
return null;
}
if (!isCell(node.props) && !isObj(node.props)) {
if (!isCell(node.props) && !isObject(node.props)) {
node = { ...node, props: {} };
}
if (!isCell(node.children) && !Array.isArray(node.children)) {
Expand Down
10 changes: 4 additions & 6 deletions html/test/assert.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isRecord } from "@commontools/utils/types";

export class AssertionError<A, E> extends Error {
actual: A | undefined;
expected: E | undefined;
Expand Down Expand Up @@ -43,7 +45,7 @@ export const matchObject = (
expected: unknown,
message = "",
) => {
if (!isObject(actual) || !isObject(expected)) {
if (!isRecord(actual) || !isRecord(expected)) {
throw new AssertionError({
message: message || "Both arguments must be objects",
actual,
Expand All @@ -61,7 +63,7 @@ export const matchObject = (
});
}

if (isObject(expected[key]) && isObject(actual[key])) {
if (isRecord(expected[key]) && isRecord(actual[key])) {
// Recursively check nested objects
matchObject(actual[key], expected[key], message);
} else if (actual[key] !== expected[key]) {
Expand All @@ -84,10 +86,6 @@ export const matchObject = (
}
};

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

export const throws = (run: () => void, message = "") => {
try {
run();
Expand Down
54 changes: 28 additions & 26 deletions iframe-sandbox/src/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
import { type JSONSchema } from "@commontools/builder";
import { isObject } from "@commontools/utils/types";

export const isJSONSchema = (source: unknown): source is JSONSchema => {
if (!isObject(source)) {
return false;
}

if (!("type" in source) || !source.type) {
return "anyOf" in source && Array.isArray(source.anyOf);
}

switch (source.type) {
case "object":
case "array":
case "string":
case "integer":
case "number":
case "boolean":
case "null":
return true;
default:
return false;
}
};

// Types used by the `common-iframe-sandbox` IPC.

Expand Down Expand Up @@ -107,34 +131,12 @@ export function isGuestError(e: object): e is GuestError {
"stacktrace" in e && typeof e.stacktrace === "string";
}

const isObject = (source: unknown): source is Record<string, unknown> =>
typeof source === "object" && source != null;
export const isTaskPerform = (source: unknown): source is TaskPerform =>
isObject(source) &&
typeof source?.intent === "string" &&
typeof source?.description === "string" &&
isObject(source?.input) &&
isJSONSchema(source?.output);

export const isJSONSchema = (source: unknown): source is JSONSchema => {
if (!isObject(source)) {
return false;
}

switch (source?.type) {
case "object":
case "array":
case "string":
case "integer":
case "number":
case "boolean":
case "null":
return true;
default: {
return Array.isArray(source?.anyOf);
}
}
};
"intent" in source && typeof source.intent === "string" &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: isn't "..." in source redundant here, since isObject(source) already makes sure this is an object and typeof source.<..> would be "undefined" if the key doesn't exist?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I thought -- the Record type (where Record<number, unknown> could be an array) allows this but object types do not (and had to add it in a few places) -- maybe there's a better way to do this? but we do sometimes explicitly want an object-like ({}) things and not an array or a record

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd get a Property 'intent' does not exist on type 'object' otherwise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, works for me:

image.png

the main problem is that for x = null it'll throw. it's really weird, because it even works for x being a string or a number. but I think you'd be fine if isObject() is true.

(also, doesn't matter for this PR, this got me into TIL mode)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd get a Property 'intent' does not exist on type 'object' otherwise

This is a typescript check, and we check that x is not null (isObject(..)) and TS null is not object (unlike JS) -- usually we want those checks ("property does not exist"), though using isRecord previously allowed any key (TIL as well)

"description" in source && typeof source.description === "string" &&
"input" in source && isObject(source.input) &&
"output" in source && isJSONSchema(source.output);

export enum HostMessageType {
Ping = "ping",
Expand Down
4 changes: 2 additions & 2 deletions jumble/src/iframe-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
removeAction,
} from "@commontools/runner";
import { DEFAULT_IFRAME_MODELS, LLMClient } from "@commontools/llm";
import { isObj } from "@commontools/utils";
import { isObject } from "@commontools/utils/types";
import {
completeJob,
failJob,
Expand Down Expand Up @@ -196,7 +196,7 @@ export const setupIframe = () =>
: undefined;
const type = context.key(key).schema?.type ??
currentValueType ?? typeof value;
if (type === "object" && isObj(value) && !Array.isArray(value)) {
if (type === "object" && isObject(value)) {
context.key(key).update(value);
} else if (
(type === "array" && Array.isArray(value)) ||
Expand Down
4 changes: 2 additions & 2 deletions jumble/src/views/CharmDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "@commontools/charm";
import { useCharmReferences } from "@/hooks/use-charm-references.ts";
import { isCell, isStream } from "@commontools/runner";
import { isObj } from "@commontools/utils";
import { isObject } from "@commontools/utils/types";
import {
CheckboxToggle,
CommonCheckbox,
Expand Down Expand Up @@ -1339,7 +1339,7 @@ function translateCellsAndStreamsToPlainJSON(
result = data.map((value) =>
translateCellsAndStreamsToPlainJSON(value, partial, complete)
);
} else if (isObj(data)) {
} else if (isObject(data)) {
result = Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
Expand Down
2 changes: 1 addition & 1 deletion memory/space-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
SchemaContext,
SchemaQuery,
} from "./interface.ts";
import { isNumber, isObject, isString } from "./util.ts";
import { isNumber, isObject, isString } from "@commontools/utils/types";
import { FactSelector, SelectAll, selectFacts, Session } from "./space.ts";
import { FactAddress } from "../runner/src/storage/cache.ts";
import {
Expand Down
5 changes: 3 additions & 2 deletions memory/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { isAlias } from "../builder/src/index.ts";
import { JSONObject, JSONValue } from "./consumer.ts";
import { isObject } from "./provider.ts";
import { isObject } from "@commontools/utils/types";

export class CycleTracker<K> {
private partial: Set<K>;
Expand Down Expand Up @@ -361,7 +361,8 @@ export function isPointer(value: any): boolean {
* @returns {boolean}
*/
function isJSONCellLink(value: any): value is JSONCellLink {
return (isObject(value) && isObject(value.cell) && "/" in value.cell &&
return (isObject(value) && "cell" in value && isObject(value.cell) &&
"/" in value.cell && "path" in value &&
Array.isArray(value.path));
}

Expand Down
12 changes: 0 additions & 12 deletions memory/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,3 @@ export const fromDID = async <ID extends DIDKey>(
}
}
};

export function isObject(value: unknown): boolean {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function isNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}

export function isString(value: unknown): value is string {
return typeof value === "string";
}
2 changes: 1 addition & 1 deletion runner/src/cfc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This is a simple form of the policy, where you express the basics of the partial ordering.

import type { JSONSchema, JSONValue } from "@commontools/builder";
import { isObject } from "../../memory/util.ts";
import { isObject } from "@commontools/utils/types";
import { extractDefaultValues } from "./utils.ts";

// We'll often work with the transitive closure of this graph.
Expand Down
2 changes: 1 addition & 1 deletion runner/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { VolatileStorageProvider } from "./storage/volatile.ts";
import { Signer } from "@commontools/identity";
import { isBrowser } from "@commontools/utils/env";
import { sleep } from "@commontools/utils/sleep";
import { defer } from "@commontools/utils/defer";
import { TransactionResult } from "@commontools/memory";
import { refer } from "@commontools/memory/reference";
import { defer } from "@commontools/utils";
import { SchemaContext, SchemaNone } from "@commontools/memory/interface";

export function log(fn: () => any[]) {
Expand Down
2 changes: 1 addition & 1 deletion toolshed/routes/ai/spell/handlers/find-spell-by-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { captureException } from "@sentry/deno";
import { FindSpellBySchemaRequest } from "@/routes/ai/spell/spell.handlers.ts";

import { checkSchemaMatch } from "@/lib/schema-match.ts";
import { isObject } from "@/routes/ai/spell/schema.ts";
import { isObject } from "@commontools/utils/types";
import { Schema } from "jsonschema";
import { Recipe, RecipeSchema } from "../spell.ts";

Expand Down
5 changes: 1 addition & 4 deletions toolshed/routes/ai/spell/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { checkSchemaMatch } from "@/lib/schema-match.ts";
import { Logger } from "@/lib/prefixed-logger.ts";
import { isObject } from "@commontools/utils/types";

export interface SchemaMatch<T = Record<string, unknown>> {
key: string;
Expand Down Expand Up @@ -91,10 +92,6 @@ export function findExactSubtreeMatches(
return matches;
}

export function isObject(value: unknown): boolean {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function decomposeSchema(
schema: Record<string, unknown>,
parentPath: string[] = [],
Expand Down
5 changes: 1 addition & 4 deletions utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
export * from "./defer.ts";
export * from "./env.ts";
export * from "./isObj.ts";
export * from "./sleep.ts";
throw new Error(`Import individual exports for utils: \`import { defer } from "@commontools/utils/defer"\``);
5 changes: 0 additions & 5 deletions utils/src/isObj.ts

This file was deleted.

24 changes: 24 additions & 0 deletions utils/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
// Predicate for narrowing a `Record` type, with string,
// symbol, or number (arrays) keys.
export function isRecord(
value: unknown,
): value is Record<string | number | symbol, unknown> {
return typeof value === "object" && value !== null;
}

// Predicate for narrowing a non-array/non-null `object` type.
export function isObject(value: unknown): value is object {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

// Predicate for narrowing a `number` type.
export function isNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}

// Predicate for narrowing a `string` type.
export function isString(value: unknown): value is string {
return typeof value === "string";
}

// Helper type to recursively remove `readonly` properties from type `T`.
export type Mutable<T> = T extends ReadonlyArray<infer U> ? Mutable<U>[]
: T extends object ? ({ -readonly [P in keyof T]: Mutable<T[P]> })
: T;