Skip to content

Commit c3228ba

Browse files
committed
[lib0/schema] support optionals in object
1 parent ca8d4b5 commit c3228ba

File tree

9 files changed

+107
-47
lines changed

9 files changed

+107
-47
lines changed

component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const parseAttrVal = (val, type) => {
101101
}
102102

103103
/**
104+
* @template S
104105
* @typedef {Object} CONF
105106
* @property {string?} [CONF.template] Template for the shadow dom.
106107
* @property {string} [CONF.style] shadow dom style. Is only used when
@@ -118,7 +119,6 @@ const parseAttrVal = (val, type) => {
118119
* to event listener.
119120
* @property {function(S, S, Lib0Component<S>):Object<string,string>} [CONF.slots] Fill slots
120121
* automatically when state changes. Maps from slot-name to slot-html.
121-
* @template S
122122
*/
123123

124124
/**

diff.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ import { equalityStrict } from './function.js'
1818
* a === b // values match
1919
* ```
2020
*
21+
* @template {string} T
2122
* @typedef {Object} SimpleDiff
2223
* @property {Number} index The index where changes were applied
2324
* @property {Number} remove The number of characters to delete starting
2425
* at `index`.
2526
* @property {T} insert The new text to insert at `index` after applying
26-
* `delete`
27-
*
28-
* @template T
2927
*/
3028

3129
const highSurrogateRegex = /[\uD800-\uDBFF]/

package-lock.json

Lines changed: 22 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,13 +505,13 @@
505505
"isomorphic.js": "^0.2.4"
506506
},
507507
"devDependencies": {
508-
"@types/node": "^18.14.0",
508+
"@types/node": "^24.0.14",
509509
"c8": "^10.1.3",
510510
"jsdoc-api": "^8.0.0",
511511
"jsdoc-plugin-typescript": "^2.2.1",
512512
"rollup": "^2.42.1",
513513
"standard": "^17.1.0",
514-
"typescript": "^5.0.2"
514+
"typescript": "^5.8.3"
515515
},
516516
"scripts": {
517517
"clean": "rm -rf dist *.d.ts */*.d.ts *.d.ts.map */*.d.ts.map",

schema.js

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,12 @@ import * as env from './environment.js'
1919

2020
/**
2121
* @template T
22-
* @typedef {T extends $Schema<infer X> ? X : T} TypeOfSchema
22+
* @typedef {T extends $Schema<infer X> ? X : T} Unwrap
2323
*/
2424

2525
/**
2626
* @template {readonly unknown[]} T
27-
* @typedef {T extends []
28-
* ? {}
29-
* : T extends [infer First]
30-
* ? First
31-
* : T extends [infer First, ...infer Rest]
32-
* ? First & Intersect<Rest>
33-
* : never
34-
* } Intersect
35-
*/
36-
37-
/**
38-
* @template {readonly unknown[]} T
39-
* @typedef {T extends readonly [$Schema<infer First>, ...infer Rest] ? [First, ...ExtractTypesFromSchemaArray<Rest>] : [] } ExtractTypesFromSchemaArray
27+
* @typedef {T extends readonly [$Schema<infer First>, ...infer Rest] ? [First, ...UnwrapArray<Rest>] : [] } UnwrapArray
4028
*/
4129

4230
/**
@@ -54,6 +42,18 @@ import * as env from './environment.js'
5442
* @typedef {Arr extends [...infer Fs, unknown] ? Fs : never} TuplePop
5543
*/
5644

45+
/**
46+
* @template {readonly unknown[]} T
47+
* @typedef {T extends []
48+
* ? {}
49+
* : T extends [infer First]
50+
* ? First
51+
* : T extends [infer First, ...infer Rest]
52+
* ? First & Intersect<Rest>
53+
* : never
54+
* } Intersect
55+
*/
56+
5757
const schemaSymbol = Symbol('0schema')
5858

5959
/**
@@ -93,10 +93,10 @@ export class $Schema {
9393
}
9494

9595
/**
96-
* @type {$Schema<T|undefined>}
96+
* @type {$Optional<$Schema<T>>}
9797
*/
9898
get optional () {
99-
return union(this, undefined_)
99+
return new $Optional(/** @type {$Schema<T>} */ (this))
100100
}
101101

102102
/**
@@ -192,9 +192,38 @@ export class $Literal extends $Schema {
192192
*/
193193
export const literal = (...literals) => new $Literal(literals)
194194

195+
const isOptionalSymbol = Symbol('optional')
196+
/**
197+
* @template {$Schema<any>} S
198+
* @extends $Schema<Unwrap<S>|undefined>
199+
*/
200+
class $Optional extends $Schema {
201+
/**
202+
* @param {S} s
203+
*/
204+
constructor (s) {
205+
super()
206+
this.s = s
207+
}
208+
209+
/**
210+
* @param {any} o
211+
* @return {o is (Unwrap<S>|undefined)}
212+
*/
213+
check (o) {
214+
return o === undefined || this.s.check(o)
215+
}
216+
get [isOptionalSymbol] () { return true }
217+
}
218+
219+
/**
220+
* @template {{ [key: string|symbol|number]: $Schema<any> }} S
221+
* @typedef {{ [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? Key : never]?: S[Key] extends $Optional<$Schema<infer Type>> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? never : Key]: S[Key] extends $Schema<infer Type> ? Type : never }} $ObjectToType
222+
*/
223+
195224
/**
196225
* @template {{[key:string|symbol|number]: $Schema<any>}} S
197-
* @extends {$Schema<{ [Key in keyof S]: S[Key] extends $Schema<infer Type> ? Type : never }>}
226+
* @extends {$Schema<$ObjectToType<S>>}
198227
*/
199228
export class $Object extends $Schema {
200229
/**
@@ -207,19 +236,21 @@ export class $Object extends $Schema {
207236

208237
/**
209238
* @param {any} o
210-
* @return {o is { [K in keyof S]: S[K] extends $Schema<infer Type> ? Type : never }}
239+
* @return {o is $ObjectToType<S>}
211240
*/
212241
check (o) {
213242
return o != null && obj.every(this.v, (vv, vk) => vv.check(o[vk]))
214243
}
215244
}
216245

246+
// I used an explicit type annotation instead of $ObjectToType, so that the user doesn't see the
247+
// weird type definitions when inspecting type definions.
217248
/**
218-
* @template {{ [key:string|symbol|number]: $Schema<any> }} T
219-
* @param {T} def
220-
* @return {CastToSchema<$Object<T>>}
249+
* @template {{ [key:string|symbol|number]: $Schema<any> }} S
250+
* @param {S} def
251+
* @return {$Schema<{ [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? Key : never]?: S[Key] extends $Optional<$Schema<infer Type>> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? never : Key]: S[Key] extends $Schema<infer Type> ? Type : never }>}
221252
*/
222-
export const object = def => new $Object(def)
253+
export const object = def => /** @type {any} */ (new $Object(def))
223254

224255
/**
225256
* @template {$Schema<string|number|symbol>} Keys
@@ -347,7 +378,7 @@ export const instance = c => new $InstanceOf(c)
347378

348379
/**
349380
* @template {$Schema<any>[]} Args
350-
* @typedef {(...args:ExtractTypesFromSchemaArray<TuplePop<Args>>)=>TypeOfSchema<TupleLast<Args>>} _LArgsToLambdaDef
381+
* @typedef {(...args:UnwrapArray<TuplePop<Args>>)=>Unwrap<TupleLast<Args>>} _LArgsToLambdaDef
351382
*/
352383

353384
/**
@@ -377,13 +408,13 @@ export class $Lambda extends $Schema {
377408
/**
378409
* @template {$Schema<any>[]} Args
379410
* @param {Args} args
380-
* @return {$Schema<(...args:ExtractTypesFromSchemaArray<TuplePop<Args>>)=>TypeOfSchema<TupleLast<Args>>>}
411+
* @return {$Schema<(...args:UnwrapArray<TuplePop<Args>>)=>Unwrap<TupleLast<Args>>>}
381412
*/
382413
export const lambda = (...args) => new $Lambda(args.length > 0 ? args : [void_])
383414

384415
/**
385416
* @template {Array<$Schema<any>>} T
386-
* @extends {$Schema<Intersect<ExtractTypesFromSchemaArray<T>>>}
417+
* @extends {$Schema<Intersect<UnwrapArray<T>>>}
387418
*/
388419
export class $Intersection extends $Schema {
389420
/**
@@ -399,7 +430,7 @@ export class $Intersection extends $Schema {
399430

400431
/**
401432
* @param {any} o
402-
* @return {o is Intersect<ExtractTypesFromSchemaArray<T>>}
433+
* @return {o is Intersect<UnwrapArray<T>>}
403434
*/
404435
check (o) {
405436
// @ts-ignore

schema.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,23 @@ export const testSchemas = _tc => {
225225
t.assert(!$fun3.validate(/** @type {(a: number, b: number) => undefined} */ (a, b) => undefined)) // too many parameters
226226
})
227227
}
228+
229+
/**
230+
* @param {t.TestCase} _tc
231+
*/
232+
export const testObjectSchemaOptionals = _tc => {
233+
const schema = s.object({ a: s.number.optional, b: s.string.optional })
234+
t.assert(schema.validate({ })) // should work
235+
// @ts-expect-error
236+
t.assert(!schema.validate({ a: 'str' })) // should throw a type error
237+
const def = s.union(s.string,s.array(s.number))
238+
const defOptional = def.optional
239+
const defObject = s.object({ j: defOptional, k: def })
240+
// @ts-expect-error
241+
t.assert(!defObject.validate({ k: undefined }))
242+
t.assert(defObject.validate({ k: [42]}))
243+
// @ts-expect-error
244+
t.assert(!defObject.validate({ k: [42], j: 42}))
245+
t.assert(defObject.validate({ k: [42], j: 'str'}))
246+
t.assert(defObject.validate({ k: [42], j: undefined}))
247+
}

set.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ export const toArray = set => Array.from(set)
1616
/**
1717
* @template T
1818
* @param {Set<T>} set
19-
* @return {T}
19+
* @return {T|undefined}
2020
*/
21-
export const first = set =>
22-
set.values().next().value ?? undefined
21+
export const first = set => set.values().next().value
2322

2423
/**
2524
* @template T

testing.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* * runTests automatically tests all exported functions that start with "test".
3232
* * The name of the function should be in camelCase and is used for the logging output.
3333
* *
34-
* * @param {t.TestCase} tc
34+
* * @ param {t.TestCase} tc
3535
* *\/
3636
* export const testMyFirstTest = tc => {
3737
* t.compare({ a: 4 }, { a: 4 }, 'objects are equal')

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"target": "ES2022",
44
"lib": ["ES2022", "dom"],
5-
"module": "ES2022",
5+
"module": "NodeNext",
66
"allowJs": true,
77
"checkJs": true,
88
"declaration": true,

0 commit comments

Comments
 (0)