@atproto/lex-schema 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/core/schema.d.ts +5 -11
  3. package/dist/core/schema.d.ts.map +1 -1
  4. package/dist/core/schema.js +10 -16
  5. package/dist/core/schema.js.map +1 -1
  6. package/dist/core/validation-error.d.ts +2 -2
  7. package/dist/core/validation-error.d.ts.map +1 -1
  8. package/dist/core/validation-error.js +1 -1
  9. package/dist/core/validation-error.js.map +1 -1
  10. package/dist/core/validation-issue.d.ts.map +1 -1
  11. package/dist/core/validation-issue.js +19 -38
  12. package/dist/core/validation-issue.js.map +1 -1
  13. package/dist/helpers.d.ts +4 -3
  14. package/dist/helpers.d.ts.map +1 -1
  15. package/dist/helpers.js +6 -1
  16. package/dist/helpers.js.map +1 -1
  17. package/dist/schema/payload.d.ts.map +1 -1
  18. package/dist/schema/payload.js +2 -3
  19. package/dist/schema/payload.js.map +1 -1
  20. package/dist/schema/record.d.ts +6 -8
  21. package/dist/schema/record.d.ts.map +1 -1
  22. package/dist/schema/record.js +1 -1
  23. package/dist/schema/record.js.map +1 -1
  24. package/dist/schema/regexp.d.ts +3 -2
  25. package/dist/schema/regexp.d.ts.map +1 -1
  26. package/dist/schema/regexp.js +6 -4
  27. package/dist/schema/regexp.js.map +1 -1
  28. package/dist/schema/string.d.ts.map +1 -1
  29. package/dist/schema/string.js +9 -2
  30. package/dist/schema/string.js.map +1 -1
  31. package/dist/schema/typed-object.d.ts +5 -7
  32. package/dist/schema/typed-object.d.ts.map +1 -1
  33. package/dist/schema/typed-object.js +1 -1
  34. package/dist/schema/typed-object.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/core/$type.test.ts +9 -5
  37. package/src/core/schema.ts +19 -16
  38. package/src/core/validation-error.ts +2 -2
  39. package/src/core/validation-issue.ts +20 -36
  40. package/src/helpers.ts +7 -1
  41. package/src/schema/array.test.ts +1 -1
  42. package/src/schema/params.test.ts +2 -2
  43. package/src/schema/payload.ts +2 -3
  44. package/src/schema/record.test.ts +135 -17
  45. package/src/schema/record.ts +14 -9
  46. package/src/schema/regexp.ts +14 -4
  47. package/src/schema/string.ts +8 -2
  48. package/src/schema/typed-object.test.ts +77 -0
  49. package/src/schema/typed-object.ts +11 -10
@@ -1,5 +1,8 @@
1
1
  import { ifCid, isLegacyBlobRef, isPlainObject } from '@atproto/lex-data'
2
2
 
3
+ const STRING_PREVIEW_MAX_LENGTH = 48
4
+ const STRING_PREVIEW_TRUNCATED_SUFFIX = '…'
5
+
3
6
  /**
4
7
  * Abstract base class for all validation issues.
5
8
  *
@@ -120,7 +123,7 @@ export class IssueInvalidType extends Issue {
120
123
  }
121
124
 
122
125
  override get message(): string {
123
- return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type (got ${stringifyType(this.input)})`
126
+ return `Expected ${oneOf(this.expected.map(stringifyExpectedType))} value type (got ${stringifyValue(this.input)})`
124
127
  }
125
128
 
126
129
  toJSON() {
@@ -289,55 +292,36 @@ function oneOf(arr: readonly string[]): string {
289
292
  return `one of ${arr.slice(0, -1).join(', ')} or ${arr.at(-1)}`
290
293
  }
291
294
 
292
- function stringifyType(value: unknown): string {
293
- switch (typeof value) {
294
- case 'object':
295
- if (value === null) return 'null'
296
- if (Array.isArray(value)) return 'array'
297
- if (ifCid(value)) return 'cid'
298
- if (isLegacyBlobRef(value)) return 'legacy-blob'
299
- if (value instanceof Date) return 'date'
300
- if (value instanceof RegExp) return 'regexp'
301
- if (value instanceof Map) return 'map'
302
- if (value instanceof Set) return 'set'
303
- return 'object'
304
- case 'number':
305
- if (Number.isInteger(value) && Number.isSafeInteger(value)) {
306
- return 'integer'
307
- }
308
- if (Number.isNaN(value)) {
309
- return 'NaN'
310
- }
311
- if (value === Infinity) {
312
- return 'Infinity'
313
- }
314
- if (value === -Infinity) {
315
- return '-Infinity'
316
- }
317
- return 'float'
318
- default:
319
- return typeof value
320
- }
321
- }
322
-
323
295
  function stringifyValue(value: unknown): string {
324
296
  switch (typeof value) {
325
297
  case 'bigint':
326
298
  return `${value}n`
327
299
  case 'number':
328
- case 'string':
329
300
  case 'boolean':
330
- return JSON.stringify(value)
301
+ return String(value)
302
+ case 'string':
303
+ return JSON.stringify(
304
+ value.length < STRING_PREVIEW_MAX_LENGTH
305
+ ? value
306
+ : `${value.slice(0, STRING_PREVIEW_MAX_LENGTH - STRING_PREVIEW_TRUNCATED_SUFFIX.length)}${STRING_PREVIEW_TRUNCATED_SUFFIX}`,
307
+ )
331
308
  case 'object':
309
+ if (value === null) return 'null'
332
310
  if (Array.isArray(value)) {
333
311
  return `[${stringifyArray(value, stringifyValue)}]`
334
312
  }
335
313
  if (isPlainObject(value)) {
336
314
  return `{${stringifyArray(Object.entries(value), stringifyObjectEntry)}}`
337
315
  }
338
- // fallthrough
316
+ if (ifCid(value)) return 'cid'
317
+ if (isLegacyBlobRef(value)) return 'legacy-blob'
318
+ if (value instanceof Date) return 'date'
319
+ if (value instanceof RegExp) return 'regexp'
320
+ if (value instanceof Map) return 'map'
321
+ if (value instanceof Set) return 'set'
322
+ return 'object'
339
323
  default:
340
- return stringifyType(value)
324
+ return typeof value
341
325
  }
342
326
  }
343
327
 
package/src/helpers.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  Subscription,
10
10
  object,
11
11
  optional,
12
+ regexp,
12
13
  string,
13
14
  } from './schema.js'
14
15
 
@@ -103,7 +104,12 @@ export type InferMethodError<
103
104
  M extends Procedure | Query | Subscription = Procedure | Query | Subscription,
104
105
  > = M extends { errors: readonly (infer E extends string)[] } ? E : never
105
106
 
107
+ /**
108
+ * @see {@link https://atproto.com/specs/xrpc#error-responses}
109
+ */
106
110
  export const lexErrorDataSchema = object({
107
- error: string({ minLength: 1 }),
111
+ // type name of the error (generic ASCII constant, no whitespace)
112
+ error: regexp(/^[\w_-]+$/, 'Expected ASCII constant with no whitespace'),
113
+ // description of the error, appropriate for display to humans
108
114
  message: optional(string()),
109
115
  }) satisfies Schema<LexErrorData>
@@ -81,7 +81,7 @@ describe('ArraySchema', () => {
81
81
  success: false,
82
82
  reason: expect.objectContaining({
83
83
  message: expect.stringContaining(
84
- 'Expected array value type (got integer) at $',
84
+ 'Expected array value type (got 3) at $',
85
85
  ),
86
86
  }),
87
87
  })
@@ -419,7 +419,7 @@ describe('ParamsSchema', () => {
419
419
  ['name', 'Alice'],
420
420
  ['bools', 'notabool'],
421
421
  ]),
422
- ).toThrow('Expected boolean value type (got string) at $.bools')
422
+ ).toThrow('Expected boolean value type (got "notabool") at $.bools')
423
423
 
424
424
  expect(() =>
425
425
  schema.fromURLSearchParams(
@@ -431,7 +431,7 @@ describe('ParamsSchema', () => {
431
431
  path: ['foo', 'bar'],
432
432
  },
433
433
  ),
434
- ).toThrow('Expected boolean value type (got string) at $.foo.bar.bools')
434
+ ).toThrow('Expected boolean value type (got "2") at $.foo.bar.bools')
435
435
  })
436
436
 
437
437
  it('ignores empty string values', () => {
@@ -109,10 +109,9 @@ export class Payload<
109
109
  matchesEncoding(contentType: string | undefined): boolean {
110
110
  const { encoding } = this
111
111
 
112
- // Handle undefined cases
113
112
  if (encoding === undefined) {
114
- // Expecting no body
115
- return contentType == null
113
+ // When the output is not defined, we don't enforce any rule on the payload.
114
+ return true
116
115
  } else if (contentType == null) {
117
116
  // Expecting a body, but got no content-type
118
117
  return false
@@ -1,6 +1,8 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { Infer, Unknown$Type, Unknown$TypedObject } from '../core.js'
1
+ import { describe, expect, expectTypeOf, it } from 'vitest'
2
+ import { DidString, Infer, Unknown$Type, Unknown$TypedObject } from '../core.js'
3
+ import { integer } from './integer.js'
3
4
  import { object } from './object.js'
5
+ import { optional } from './optional.js'
4
6
  import { record } from './record.js'
5
7
  import { string } from './string.js'
6
8
 
@@ -123,41 +125,157 @@ describe('RecordSchema', () => {
123
125
  })
124
126
  })
125
127
 
126
- describe('build method', () => {
128
+ describe('build() method', () => {
127
129
  const schema = record(
128
130
  'any',
129
- 'app.bsky.feed.post',
131
+ 'io.example.record',
130
132
  object({
131
- $type: string(),
132
- text: string(),
133
+ actor: string({ format: 'did' }),
134
+ text: optional(string()),
133
135
  }),
134
136
  )
135
137
 
136
- it('adds correct $type to input', () => {
137
- const result = schema.build({ text: 'Hello world' })
138
- expect(result.$type).toBe('app.bsky.feed.post')
139
- expect(result.text).toBe('Hello world')
138
+ it('adds $type to input', () => {
139
+ const result = schema.build({
140
+ actor: 'did:foo:bar',
141
+ text: 'Hello',
142
+ })
143
+ expect(result).toStrictEqual({
144
+ $type: 'io.example.record',
145
+ actor: 'did:foo:bar',
146
+ text: 'Hello',
147
+ })
148
+ expectTypeOf(result).toEqualTypeOf<{
149
+ $type: 'io.example.record'
150
+ actor: DidString
151
+ text?: string
152
+ }>()
153
+ })
154
+
155
+ it('causes a type error for invalid values', () => {
156
+ const result = schema.build({
157
+ // @ts-expect-error
158
+ actor: 3,
159
+ text: 'Hello',
160
+ })
161
+ expectTypeOf(result).toEqualTypeOf<{
162
+ $type: 'io.example.record'
163
+ actor: DidString
164
+ text?: string
165
+ }>()
140
166
  })
141
167
 
142
168
  it('preserves existing properties', () => {
143
169
  const result = schema.build({
144
- text: 'Hello world',
170
+ actor: 'did:foo:bar',
145
171
  // @ts-expect-error
146
172
  extra: 'value',
147
173
  })
148
- expect(result.$type).toBe('app.bsky.feed.post')
149
- expect(result.text).toBe('Hello world')
150
- // @ts-expect-error
151
- expect(result.extra).toBe('value')
174
+ expect(result).toStrictEqual({
175
+ $type: 'io.example.record',
176
+ actor: 'did:foo:bar',
177
+ extra: 'value',
178
+ })
179
+ expectTypeOf(result).toEqualTypeOf<{
180
+ actor: `did:${string}:${string}`
181
+ text?: string | undefined
182
+ $type: 'io.example.record'
183
+ }>()
152
184
  })
153
185
 
154
186
  it('overwrites existing $type', () => {
155
187
  const result = schema.build({
156
188
  // @ts-expect-error
157
189
  $type: 'wrong.type',
158
- text: 'Hello world',
190
+ actor: 'did:foo:bar',
191
+ })
192
+ expect(result).toStrictEqual({
193
+ $type: 'io.example.record',
194
+ actor: 'did:foo:bar',
195
+ })
196
+ expectTypeOf(result).toEqualTypeOf<{
197
+ $type: 'io.example.record'
198
+ actor: DidString
199
+ text?: string
200
+ }>()
201
+ })
202
+
203
+ describe('build() does not validate', () => {
204
+ const schema = record(
205
+ 'any',
206
+ 'io.example.record',
207
+ object({
208
+ actor: string({ format: 'did' }),
209
+ count: integer(),
210
+ }),
211
+ )
212
+
213
+ it('does not throw for invalid data', () => {
214
+ const result = schema.build({
215
+ // @ts-expect-error
216
+ actor: 'not-a-did',
217
+ count: 123,
218
+ })
219
+
220
+ expect(result.$type).toBe('io.example.record')
221
+ expect(result.actor).toBe('not-a-did')
222
+ expect(result.count).toBe(123)
223
+ })
224
+
225
+ it('does not throw for invalid types', () => {
226
+ const result = schema.build({
227
+ actor: 'did:plc:abc123',
228
+ // @ts-expect-error
229
+ count: 'not-a-number',
230
+ })
231
+
232
+ expect(result.$type).toBe('io.example.record')
233
+ expect(result.count).toBe('not-a-number')
234
+ })
235
+
236
+ it('does not throw for missing required fields', () => {
237
+ // @ts-expect-error
238
+ const result = schema.build({})
239
+
240
+ expect(result.$type).toBe('io.example.record')
241
+ expect(result.actor).toBeUndefined()
242
+ expect(result.count).toBeUndefined()
243
+ })
244
+
245
+ it('does not throw for extra fields', () => {
246
+ const result = schema.build({
247
+ actor: 'did:plc:abc123',
248
+ count: 42,
249
+ // @ts-expect-error
250
+ extra: 'unexpected',
251
+ })
252
+
253
+ expect(result.$type).toBe('io.example.record')
254
+
255
+ // @ts-expect-error
256
+ expect(result.extra).toBe('unexpected')
257
+ })
258
+
259
+ it('parse() still validates after build()', () => {
260
+ const built = schema.build({
261
+ // @ts-expect-error
262
+ actor: 'not-a-did',
263
+ count: 123,
264
+ })
265
+
266
+ expect(() => schema.parse(built)).toThrow('Invalid DID')
267
+ })
268
+
269
+ it('safeParse() can detect validation errors after build()', () => {
270
+ const built = schema.build({
271
+ // @ts-expect-error
272
+ actor: 'not-a-did',
273
+ count: 123,
274
+ })
275
+
276
+ const result = schema.safeParse(built)
277
+ expect(result.success).toBe(false)
159
278
  })
160
- expect(result.$type).toBe('app.bsky.feed.post')
161
279
  })
162
280
  })
163
281
 
@@ -1,3 +1,4 @@
1
+ import { LexMap } from '@atproto/lex-data'
1
2
  import {
2
3
  $Typed,
3
4
  $typed,
@@ -51,9 +52,9 @@ export type TypedRecord<
51
52
  * ```
52
53
  */
53
54
  export class RecordSchema<
54
- const TKey extends LexiconRecordKey = any,
55
- const TType extends NsidString = any,
56
- const TShape extends Validator<{ [k: string]: unknown }> = any,
55
+ const TKey extends LexiconRecordKey = LexiconRecordKey,
56
+ const TType extends NsidString = NsidString,
57
+ const TShape extends Validator<LexMap> = Validator<LexMap>,
57
58
  > extends Schema<
58
59
  $Typed<InferInput<TShape>, TType>,
59
60
  $Typed<InferOutput<TShape>, TType>
@@ -86,9 +87,13 @@ export class RecordSchema<
86
87
  }
87
88
 
88
89
  build(
89
- input: Omit<InferInput<this>, '$type'>,
90
- ): $Typed<InferOutput<this>, TType> {
91
- return this.parse($typed(input, this.$type))
90
+ input: Omit<InferOutput<TShape>, '$type'>,
91
+ ): $Typed<InferOutput<TShape>, TType>
92
+ build(
93
+ input: Omit<InferInput<TShape>, '$type'>,
94
+ ): $Typed<InferInput<TShape>, TType>
95
+ build(input: Record<string, unknown>) {
96
+ return $typed(input, this.$type)
92
97
  }
93
98
 
94
99
  isTypeOf<TValue extends { $type?: unknown }>(
@@ -199,11 +204,11 @@ type AsNsid<T> = T extends `${string}#${string}` ? never : T
199
204
  export function record<
200
205
  const K extends LexiconRecordKey,
201
206
  const T extends NsidString,
202
- const S extends Validator<{ [k: string]: unknown }>,
207
+ const S extends Validator<LexMap>,
203
208
  >(key: K, type: AsNsid<T>, validator: S): RecordSchema<K, T, S>
204
209
  export function record<
205
210
  const K extends LexiconRecordKey,
206
- const V extends { $type: NsidString },
211
+ const V extends LexMap & { $type: NsidString },
207
212
  >(
208
213
  key: K,
209
214
  type: AsNsid<V['$type']>,
@@ -213,7 +218,7 @@ export function record<
213
218
  export function record<
214
219
  const K extends LexiconRecordKey,
215
220
  const T extends NsidString,
216
- const S extends Validator<{ [k: string]: unknown }>,
221
+ const S extends Validator<LexMap>,
217
222
  >(key: K, type: T, validator: S) {
218
223
  return new RecordSchema<K, T, S>(key, type, validator)
219
224
  }
@@ -20,7 +20,10 @@ export class RegexpSchema<
20
20
  > extends Schema<TValue> {
21
21
  readonly type = 'regexp' as const
22
22
 
23
- constructor(public readonly pattern: RegExp) {
23
+ constructor(
24
+ public readonly pattern: RegExp,
25
+ public readonly message?: string,
26
+ ) {
24
27
  super()
25
28
  }
26
29
 
@@ -30,7 +33,11 @@ export class RegexpSchema<
30
33
  }
31
34
 
32
35
  if (!this.pattern.test(input)) {
33
- return ctx.issueInvalidFormat(input, this.pattern.toString())
36
+ return ctx.issueInvalidFormat(
37
+ input,
38
+ this.pattern.toString(),
39
+ this.message,
40
+ )
34
41
  }
35
42
 
36
43
  return ctx.success(input as TValue)
@@ -67,6 +74,9 @@ export class RegexpSchema<
67
74
  * ```
68
75
  */
69
76
  /*@__NO_SIDE_EFFECTS__*/
70
- export function regexp<TInput extends string = string>(pattern: RegExp) {
71
- return new RegexpSchema<TInput>(pattern)
77
+ export function regexp<TInput extends string = string>(
78
+ pattern: RegExp,
79
+ message?: string,
80
+ ): RegexpSchema<TInput> {
81
+ return new RegexpSchema<TInput>(pattern, message)
72
82
  }
@@ -99,12 +99,15 @@ export class StringSchema<
99
99
  }
100
100
  }
101
101
 
102
+ // Optimization: count graphemes once
102
103
  let lazyGraphLen: number
103
104
 
104
105
  const minGraphemes = this.options.minGraphemes
105
106
  if (minGraphemes != null) {
106
- // Optimization: avoid counting graphemes if the length check already fails
107
107
  if (str.length < minGraphemes) {
108
+ // If the JavaScript string length (UTF-16) is below the minimal limit,
109
+ // its grapheme length (which <= .length) will also be below.
110
+ // Fail early.
108
111
  return ctx.issueTooSmall(str, 'grapheme', minGraphemes, str.length)
109
112
  } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {
110
113
  return ctx.issueTooSmall(str, 'grapheme', minGraphemes, lazyGraphLen)
@@ -113,7 +116,10 @@ export class StringSchema<
113
116
 
114
117
  const maxGraphemes = this.options.maxGraphemes
115
118
  if (maxGraphemes != null) {
116
- if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
119
+ if (str.length <= maxGraphemes) {
120
+ // If the JavaScript string length (UTF-16) is within the maximum limit,
121
+ // its grapheme length (which <= .length) will also be within.
122
+ } else if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
117
123
  return ctx.issueTooBig(str, 'grapheme', maxGraphemes, lazyGraphLen)
118
124
  }
119
125
  }
@@ -303,6 +303,83 @@ describe('TypedObjectSchema', () => {
303
303
  const result = emptySchema.build(input)
304
304
  expect(result).toEqual({ $type: 'app.bsky.test' })
305
305
  })
306
+
307
+ describe('build() does not validate', () => {
308
+ const validationSchema = typedObject(
309
+ 'app.bsky.test',
310
+ 'validation',
311
+ object({
312
+ actor: string({ format: 'did' }),
313
+ count: integer(),
314
+ }),
315
+ )
316
+
317
+ it('does not throw for invalid data', () => {
318
+ const result = validationSchema.build({
319
+ // @ts-expect-error
320
+ actor: 'not-a-did',
321
+ count: 123,
322
+ })
323
+
324
+ expect(result.$type).toBe('app.bsky.test#validation')
325
+ expect(result.actor).toBe('not-a-did')
326
+ expect(result.count).toBe(123)
327
+ })
328
+
329
+ it('does not throw for invalid types', () => {
330
+ const result = validationSchema.build({
331
+ actor: 'did:plc:abc123',
332
+ // @ts-expect-error
333
+ count: 'not-a-number',
334
+ })
335
+
336
+ expect(result.$type).toBe('app.bsky.test#validation')
337
+ expect(result.count).toBe('not-a-number')
338
+ })
339
+
340
+ it('does not throw for missing required fields', () => {
341
+ // @ts-expect-error
342
+ const result = validationSchema.build({})
343
+
344
+ expect(result.$type).toBe('app.bsky.test#validation')
345
+ expect(result.actor).toBeUndefined()
346
+ expect(result.count).toBeUndefined()
347
+ })
348
+
349
+ it('does not throw for extra fields', () => {
350
+ const result = validationSchema.build({
351
+ actor: 'did:plc:abc123',
352
+ count: 42,
353
+ // @ts-expect-error
354
+ extra: 'unexpected',
355
+ })
356
+
357
+ expect(result.$type).toBe('app.bsky.test#validation')
358
+ // @ts-expect-error
359
+ expect(result.extra).toBe('unexpected')
360
+ })
361
+
362
+ it('parse() still validates after build()', () => {
363
+ const built = validationSchema.build({
364
+ // @ts-expect-error
365
+ actor: 'not-a-did',
366
+ count: 123,
367
+ })
368
+
369
+ expect(() => validationSchema.parse(built)).toThrow('Invalid DID')
370
+ })
371
+
372
+ it('safeParse() can detect validation errors after build()', () => {
373
+ const built = validationSchema.build({
374
+ // @ts-expect-error
375
+ actor: 'not-a-did',
376
+ count: 123,
377
+ })
378
+
379
+ const result = validationSchema.safeParse(built)
380
+ expect(result.success).toBe(false)
381
+ })
382
+ })
306
383
  })
307
384
 
308
385
  describe('$build method', () => {
@@ -1,4 +1,4 @@
1
- import { isPlainObject } from '@atproto/lex-data'
1
+ import { LexMap, isPlainObject } from '@atproto/lex-data'
2
2
  import {
3
3
  $Type,
4
4
  $TypeOf,
@@ -43,7 +43,7 @@ export type MaybeTypedObject<
43
43
  */
44
44
  export class TypedObjectSchema<
45
45
  const TType extends $Type = $Type,
46
- const TShape extends Validator<{ [k: string]: unknown }> = any,
46
+ const TShape extends Validator<LexMap> = Validator<LexMap>,
47
47
  > extends Schema<
48
48
  $TypedMaybe<InferInput<TShape>, TType>,
49
49
  $TypedMaybe<InferOutput<TShape>, TType>
@@ -74,12 +74,13 @@ export class TypedObjectSchema<
74
74
  }
75
75
 
76
76
  build(
77
- input: Omit<InferInput<this>, '$type'>,
78
- ): $Typed<InferOutput<this>, TType> {
79
- return this.parse($typed(input, this.$type)) as $Typed<
80
- InferOutput<this>,
81
- TType
82
- >
77
+ input: Omit<InferOutput<TShape>, '$type'>,
78
+ ): $Typed<InferOutput<TShape>, TType>
79
+ build(
80
+ input: Omit<InferInput<TShape>, '$type'>,
81
+ ): $Typed<InferInput<TShape>, TType>
82
+ build(input: Record<string, unknown>) {
83
+ return $typed(input, this.$type)
83
84
  }
84
85
 
85
86
  isTypeOf<TValue extends Record<string, unknown>>(
@@ -155,7 +156,7 @@ export class TypedObjectSchema<
155
156
  export function typedObject<
156
157
  const N extends NsidString,
157
158
  const H extends string,
158
- const S extends Validator<{ [k: string]: unknown }>,
159
+ const S extends Validator<LexMap>,
159
160
  >(nsid: N, hash: H, validator: S): TypedObjectSchema<$Type<N, H>, S>
160
161
  export function typedObject<V extends { $type?: $Type }>(
161
162
  nsid: V extends { $type?: infer T extends string }
@@ -174,7 +175,7 @@ export function typedObject<V extends { $type?: $Type }>(
174
175
  export function typedObject<
175
176
  const N extends NsidString,
176
177
  const H extends string,
177
- const S extends Validator<{ [k: string]: unknown }>,
178
+ const S extends Validator<LexMap>,
178
179
  >(nsid: N, hash: H, validator: S) {
179
180
  return new TypedObjectSchema<$Type<N, H>, S>($type(nsid, hash), validator)
180
181
  }