diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index bf025e75..85fecd19 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -1,5 +1,5 @@ export * from "loro-wasm"; -import { Container, ContainerType, Delta, OpId, Value } from "loro-wasm"; +import { Container, ContainerType, Delta, LoroText, LoroTree, OpId, Value } from "loro-wasm"; import { PrelimText, PrelimList, PrelimMap } from "loro-wasm"; import { ContainerID, Loro, LoroList, LoroMap, TreeID } from "loro-wasm"; @@ -89,8 +89,8 @@ export type MapDiff = { export type TreeDiff = { type: "tree"; diff: - | { target: TreeID; action: "create" | "delete" } - | { target: TreeID; action: "move"; parent: TreeID }; + | { target: TreeID; action: "create" | "delete" } + | { target: TreeID; action: "move"; parent: TreeID }; }; export type Diff = ListDiff | TextDiff | MapDiff | TreeDiff; @@ -107,21 +107,61 @@ export function isContainerId(s: string): s is ContainerID { export { Loro }; +/** Whether the value is a container. + * + * # Example + * + * ```ts + * const doc = new Loro(); + * const map = doc.getMap("map"); + * const list = doc.getList("list"); + * const text = doc.getText("text"); + * isContainer(map); // true + * isContainer(list); // true + * isContainer(text); // true + * isContainer(123); // false + * isContainer("123"); // false + * isContainer({}); // false + */ export function isContainer(value: any): value is Container { if (typeof value !== "object" || value == null) { return false; } const p = value.__proto__; - return p.hasOwnProperty("kind") && CONTAINER_TYPES.includes(value.kind()); + if (p == null || typeof p !== "object" || typeof p["kind"] !== "function") { + return false; + } + + return CONTAINER_TYPES.includes(value.kind()); } -export function valueType(value: any): "Json" | ContainerType { +/** Get the type of a value that may be a container. + * + * # Example + * + * ```ts + * const doc = new Loro(); + * const map = doc.getMap("map"); + * const list = doc.getList("list"); + * const text = doc.getText("text"); + * getType(map); // "Map" + * getType(list); // "List" + * getType(text); // "Text" + * getType(123); // "Json" + * getType("123"); // "Json" + * getType({}); // "Json" + * ``` + */ +export function getType(value: T): T extends LoroText ? "Text" : + T extends LoroMap ? "Map" : + T extends LoroTree ? "Tree" : + T extends LoroList ? "List" : "Json" { if (isContainer(value)) { return value.kind(); } - return "Json"; + return "Json" as any; } declare module "loro-wasm" { diff --git a/loro-js/tests/basic.test.ts b/loro-js/tests/basic.test.ts index 191b7209..09a819ab 100644 --- a/loro-js/tests/basic.test.ts +++ b/loro-js/tests/basic.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, expectTypeOf, it } from "vitest"; import { Loro, LoroList, @@ -6,7 +6,7 @@ import { isContainer, setPanicHook, toEncodedVersion, - valueType, + getType, } from "../src"; import { Container } from "../dist/loro"; @@ -106,8 +106,8 @@ describe("list", () => { list.insertContainer(2, "Text"); const t = list.toArray()[2]; expect(isContainer(t)).toBeTruthy(); - expect(valueType(t)).toBe("Text"); - expect(valueType(123)).toBe("Json"); + expect(getType(t)).toBe("Text"); + expect(getType(123)).toBe("Json"); }); }); @@ -241,3 +241,45 @@ it("handlers should still be usable after doc is dropped", () => { expect(map.toJson()).toStrictEqual({ k: 8 }); }); + +it("isContainer", () => { + expect(isContainer("123")).toBeFalsy(); + expect(isContainer(123)).toBeFalsy(); + expect(isContainer(123n)).toBeFalsy(); + expect(isContainer(new Map())).toBeFalsy(); + expect(isContainer(new Set())).toBeFalsy(); + expect(isContainer({})).toBeFalsy(); + expect(isContainer(undefined)).toBeFalsy(); + expect(isContainer(null)).toBeFalsy(); + const doc = new Loro(); + const t = doc.getText("t"); + expect(isContainer(t)).toBeTruthy(); + expect(getType(t)).toBe("Text"); + expect(getType(123)).toBe("Json"); +}) + +it("getValueType", () => { + // Type tests + const doc = new Loro(); + const t = doc.getText("t"); + expectTypeOf(getType(t)).toEqualTypeOf<"Text">(); + expect(getType(t)).toBe("Text"); + expectTypeOf(getType(123)).toEqualTypeOf<"Json">(); + expect(getType(123)).toBe("Json"); + expectTypeOf(getType(undefined)).toEqualTypeOf<"Json">(); + expect(getType(undefined)).toBe("Json"); + expectTypeOf(getType(null)).toEqualTypeOf<"Json">(); + expect(getType(null)).toBe("Json"); + expectTypeOf(getType({})).toEqualTypeOf<"Json">(); + expect(getType({})).toBe("Json"); + + const map = doc.getMap("map"); + const list = doc.getList("list"); + const tree = doc.getTree("tree"); + expectTypeOf(getType(map)).toEqualTypeOf<"Map">(); + expect(getType(map)).toBe("Map"); + expectTypeOf(getType(list)).toEqualTypeOf<"List">(); + expect(getType(list)).toBe("List"); + expectTypeOf(getType(tree)).toEqualTypeOf<"Tree">(); + expect(getType(tree)).toBe("Tree"); +})