This repository has been archived by the owner on Oct 7, 2024. It is now read-only.
generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: add
exactOptional()
superstruct type (#100)
- Loading branch information
Showing
12 changed files
with
673 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { Infer } from 'superstruct'; | ||
import { boolean, number, optional, string } from 'superstruct'; | ||
import { expectAssignable, expectNotAssignable } from 'tsd'; | ||
|
||
import { exactOptional, object } from '.'; | ||
|
||
const exactOptionalObject = object({ | ||
a: number(), | ||
b: optional(string()), | ||
c: exactOptional(boolean()), | ||
}); | ||
|
||
type ExactOptionalObject = Infer<typeof exactOptionalObject>; | ||
|
||
expectAssignable<ExactOptionalObject>({ a: 0 }); | ||
expectAssignable<ExactOptionalObject>({ a: 0, b: 'test' }); | ||
expectAssignable<ExactOptionalObject>({ a: 0, b: 'test', c: true }); | ||
expectAssignable<ExactOptionalObject>({ a: 0, b: undefined }); | ||
expectNotAssignable<ExactOptionalObject>({ a: 0, b: 'test', c: 0 }); | ||
expectNotAssignable<ExactOptionalObject>({ a: 0, b: 'test', c: undefined }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { is, literal, max, number, string, union } from 'superstruct'; | ||
|
||
import { exactOptional, object } from '.'; | ||
|
||
describe('superstruct', () => { | ||
describe('exactOptional', () => { | ||
const simpleStruct = object({ | ||
foo: exactOptional(string()), | ||
}); | ||
|
||
it.each([ | ||
{ struct: simpleStruct, obj: {}, expected: true }, | ||
{ struct: simpleStruct, obj: { foo: undefined }, expected: false }, | ||
{ struct: simpleStruct, obj: { foo: 'hi' }, expected: true }, | ||
{ struct: simpleStruct, obj: { bar: 'hi' }, expected: false }, | ||
{ struct: simpleStruct, obj: { foo: 1 }, expected: false }, | ||
])( | ||
'returns $expected for is($obj, <struct>)', | ||
({ struct, obj, expected }) => { | ||
expect(is(obj, struct)).toBe(expected); | ||
}, | ||
); | ||
|
||
const nestedStruct = object({ | ||
foo: object({ | ||
bar: exactOptional(string()), | ||
}), | ||
}); | ||
|
||
it.each([ | ||
{ struct: nestedStruct, obj: { foo: {} }, expected: true }, | ||
{ struct: nestedStruct, obj: { foo: { bar: 'hi' } }, expected: true }, | ||
{ | ||
struct: nestedStruct, | ||
obj: { foo: { bar: undefined } }, | ||
expected: false, | ||
}, | ||
])( | ||
'returns $expected for is($obj, <struct>)', | ||
({ struct, obj, expected }) => { | ||
expect(is(obj, struct)).toBe(expected); | ||
}, | ||
); | ||
|
||
const structWithUndef = object({ | ||
foo: exactOptional(union([string(), literal(undefined)])), | ||
}); | ||
|
||
it.each([ | ||
{ struct: structWithUndef, obj: {}, expected: true }, | ||
{ struct: structWithUndef, obj: { foo: undefined }, expected: true }, | ||
{ struct: structWithUndef, obj: { foo: 'hi' }, expected: true }, | ||
{ struct: structWithUndef, obj: { bar: 'hi' }, expected: false }, | ||
{ struct: structWithUndef, obj: { foo: 1 }, expected: false }, | ||
])( | ||
'returns $expected for is($obj, <struct>)', | ||
({ struct, obj, expected }) => { | ||
expect(is(obj, struct)).toBe(expected); | ||
}, | ||
); | ||
|
||
it('should support refinements', () => { | ||
const struct = object({ | ||
foo: exactOptional(max(number(), 0)), | ||
}); | ||
|
||
expect(is({ foo: 0 }, struct)).toBe(true); | ||
expect(is({ foo: -1 }, struct)).toBe(true); | ||
expect(is({ foo: 1 }, struct)).toBe(false); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* eslint-disable @typescript-eslint/naming-convention */ | ||
import { | ||
type Infer, | ||
type Context, | ||
Struct, | ||
object as stObject, | ||
} from 'superstruct'; | ||
import type { | ||
ObjectSchema, | ||
OmitBy, | ||
Optionalize, | ||
PickBy, | ||
Simplify, | ||
} from 'superstruct/dist/utils'; | ||
|
||
declare const ExactOptionalSymbol: unique symbol; | ||
|
||
export type ExactOptionalTag = { | ||
type: typeof ExactOptionalSymbol; | ||
}; | ||
|
||
/** | ||
* Exclude a type from the properties of a type. | ||
* | ||
* ```ts | ||
* type Foo = { a: string | null; b: number }; | ||
* type Bar = ExcludeType<Foo, null>; | ||
* // Bar = { a: string, b: number } | ||
* ``` | ||
*/ | ||
export type ExcludeType<T, V> = { | ||
[K in keyof T]: Exclude<T[K], V>; | ||
}; | ||
|
||
/** | ||
* Make optional all properties that have the `ExactOptionalTag` type. | ||
* | ||
* ```ts | ||
* type Foo = { a: string | ExactOptionalTag; b: number}; | ||
* type Bar = ExactOptionalize<Foo>; | ||
* // Bar = { a?: string; b: number} | ||
* ``` | ||
*/ | ||
export type ExactOptionalize<S extends object> = OmitBy<S, ExactOptionalTag> & | ||
Partial<ExcludeType<PickBy<S, ExactOptionalTag>, ExactOptionalTag>>; | ||
|
||
/** | ||
* Infer a type from an superstruct object schema. | ||
*/ | ||
export type ObjectType<S extends ObjectSchema> = Simplify< | ||
ExactOptionalize<Optionalize<{ [K in keyof S]: Infer<S[K]> }>> | ||
>; | ||
|
||
/** | ||
* Change the return type of a superstruct object struct to support exact | ||
* optional properties. | ||
* | ||
* @param schema - The object schema. | ||
* @returns A struct representing an object with a known set of properties. | ||
*/ | ||
export function object<S extends ObjectSchema>( | ||
schema: S, | ||
): Struct<ObjectType<S>, S> { | ||
return stObject(schema) as any; | ||
} | ||
|
||
/** | ||
* Check if the current property is present in its parent object. | ||
* | ||
* @param ctx - The context to check. | ||
* @returns `true` if the property is present, `false` otherwise. | ||
*/ | ||
function hasOptional(ctx: Context): boolean { | ||
const property: string = ctx.path[ctx.path.length - 1]; | ||
const parent: Record<string, unknown> = ctx.branch[ctx.branch.length - 2]; | ||
|
||
return property in parent; | ||
} | ||
|
||
/** | ||
* Augment a struct to allow exact-optional values. Exact-optional values can | ||
* be omitted but cannot be `undefined`. | ||
* | ||
* ```ts | ||
* const foo = object({ bar: exactOptional(string()) }); | ||
* type Foo = Infer<typeof foo>; | ||
* // Foo = { bar?: string } | ||
* ``` | ||
* | ||
* @param struct - The struct to augment. | ||
* @returns The augmented struct. | ||
*/ | ||
export function exactOptional<T, S>( | ||
struct: Struct<T, S>, | ||
): Struct<T | ExactOptionalTag, S> { | ||
return new Struct({ | ||
...struct, | ||
|
||
validator: (value, ctx) => | ||
!hasOptional(ctx) || struct.validator(value, ctx), | ||
|
||
refiner: (value, ctx) => | ||
!hasOptional(ctx) || struct.refiner(value as T, ctx), | ||
}); | ||
} |
Oops, something went wrong.