Skip to content

Commit

Permalink
feat: update IsObject variances
Browse files Browse the repository at this point in the history
  • Loading branch information
unional committed Oct 6, 2023
1 parent bfeefe0 commit 4dc227a
Show file tree
Hide file tree
Showing 14 changed files with 304 additions and 222 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-shrimps-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"type-plus": minor
---

Update `IsObject` and variants
2 changes: 1 addition & 1 deletion type-plus/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ export type * from './numeric/numeric_type.js'
export type { Required, RequiredExcept, RequiredPick } from './object/Required.js'
export * from './object/index.js'
export type * from './object/is_not_object.js'
export type * from './object/is_not_strict_object.js'
export type * from './object/is_object.js'
export type * from './object/is_strict_object.js'
export * as ObjectPlus from './object/object_plus.js'
export type { NotObjectType, ObjectType } from './object/object_type.js'
export * from './predicates/index.js'
export type { PrimitiveTypes } from './primitive.js'
export * from './promise/index.js'
Expand Down
44 changes: 32 additions & 12 deletions type-plus/ts/object/is_not_object.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { it } from '@jest/globals'
import { testType, type IsNotObject } from '../index.js'
import { testType, type $Else, type $Then, type IsNotObject } from '../index.js'

it('returns false if T is object', () => {
testType.false<IsNotObject<object>>(true)
Expand Down Expand Up @@ -43,20 +43,40 @@ it('returns true for all other types', () => {
testType.true<IsNotObject<1n>>(true)
})

it('returns true if T is union of object', () => {
testType.true<IsNotObject<object | 1>>(true)
it('distributes over union type', () => {
testType.equal<IsNotObject<object | 1>, boolean>(true)
testType.equal<IsNotObject<{ a: 1 } | 1>, boolean>(true)
})

it('returns false if T is intersection of object', () => {
testType.false<IsNotObject<object & { a: 1 }>>(true)
it('can disable union distribution', () => {
testType.equal<IsNotObject<{ a: 1 } | 1>, boolean>(true)
testType.equal<IsNotObject<{ a: 1 } | 1, { distributive: false }>, true>(true)
})

it('can override Then/Else', () => {
testType.equal<IsNotObject<object, 1, 2>, 2>(true)
testType.equal<IsNotObject<0, 1, 2>, 1>(true)
it('returns false for intersection type', () => {
testType.equal<object & [], object & []>(true)
testType.false<IsNotObject<object & []>>(true)
testType.false<IsNotObject<{ a: 1 } & []>>(true)
})

it('works as filter', () => {
testType.equal<IsNotObject<object, { selection: 'filter' }>, never>(true)
testType.equal<IsNotObject<{ a: 1 }, { selection: 'filter' }>, never>(true)

testType.equal<IsNotObject<never, { selection: 'filter' }>, never>(true)
testType.equal<IsNotObject<unknown, { selection: 'filter' }>, unknown>(true)
testType.equal<IsNotObject<object | boolean, { selection: 'filter' }>, boolean>(true)
testType.equal<IsNotObject<{ a: 1 } | 1n, { selection: 'filter' }>, 1n>(true)
})

it('works with unique branches', () => {
testType.equal<IsNotObject<object, IsNotObject.$Branch>, $Else>(true)
testType.equal<IsNotObject<{ a: 1 }, IsNotObject.$Branch>, $Else>(true)

testType.equal<IsNotObject<any, IsNotObject.$Branch>, $Then>(true)
testType.equal<IsNotObject<unknown, IsNotObject.$Branch>, $Then>(true)
testType.equal<IsNotObject<never, IsNotObject.$Branch>, $Then>(true)
testType.equal<IsNotObject<void, IsNotObject.$Branch>, $Then>(true)

testType.equal<IsNotObject<any, 1, 2>, 1>(true)
testType.equal<IsNotObject<unknown, 1, 2>, 1>(true)
testType.equal<IsNotObject<never, 1, 2>, 1>(true)
testType.equal<IsNotObject<void, 1, 2>, 1>(true)
testType.equal<IsNotObject<object | 1, IsNotObject.$Branch>, $Then | $Else>(true)
})
10 changes: 8 additions & 2 deletions type-plus/ts/object/is_not_object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ObjectType } from './object_type.js'
import type { SelectInvertWithDistribute } from '../type_plus/branch/select_invert_with_distribute.js'

/**
* Is `T` not an `object`.
Expand All @@ -14,4 +14,10 @@ import type { ObjectType } from './object_type.js'
* ```
*/

export type IsNotObject<T, Then = true, Else = false> = ObjectType<T, Else, Then>
export type IsNotObject<T, $O extends IsNotObject.$Options = {}> = SelectInvertWithDistribute<T, object, $O>

export namespace IsNotObject {
export type $Options = SelectInvertWithDistribute.$Options
export type $Default = SelectInvertWithDistribute.$Default
export type $Branch = SelectInvertWithDistribute.$Branch
}
84 changes: 84 additions & 0 deletions type-plus/ts/object/is_not_strict_object.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { it } from '@jest/globals'
import { testType, type $Else, type $Then, type IsNotStrictObject } from '../index.js'


it('returns false for object', () => {
testType.equal<IsNotStrictObject<object>, false>(true)
})

it('returns true for object literal', () => {
testType.equal<IsNotStrictObject<{ a: number }>, true>(true)
})

it('returns true for empty object literal', () => {
testType.equal<IsNotStrictObject<{}>, true>(true)
})

it('returns true if T is array or tuple', () => {
testType.true<IsNotStrictObject<string[]>>(true)
testType.true<IsNotStrictObject<[]>>(true)
testType.true<IsNotStrictObject<[1, 2]>>(true)
})

it('returns true for special types', () => {
testType.equal<IsNotStrictObject<any>, true>(true)
testType.equal<IsNotStrictObject<unknown>, true>(true)
testType.equal<IsNotStrictObject<never>, true>(true)
testType.equal<IsNotStrictObject<void>, true>(true)
})

it('returns true for all other types', () => {
testType.true<IsNotStrictObject<undefined>>(true)
testType.true<IsNotStrictObject<null>>(true)
testType.true<IsNotStrictObject<boolean>>(true)
testType.true<IsNotStrictObject<true>>(true)
testType.true<IsNotStrictObject<false>>(true)
testType.true<IsNotStrictObject<number>>(true)
testType.true<IsNotStrictObject<1>>(true)
testType.true<IsNotStrictObject<string>>(true)
testType.true<IsNotStrictObject<''>>(true)
testType.true<IsNotStrictObject<symbol>>(true)
testType.true<IsNotStrictObject<bigint>>(true)
testType.true<IsNotStrictObject<1n>>(true)
})

it('distributes for union type', () => {
testType.equal<IsNotStrictObject<object | 1>, boolean>(true)
testType.equal<IsNotStrictObject<object | boolean>, boolean>(true)
testType.equal<IsNotStrictObject<{ a: 1 } | 1>, true>(true)
})

it('can disable union distribution', () => {
testType.equal<IsNotStrictObject<{ a: 1 } | 1>, true>(true)
testType.equal<IsNotStrictObject<{ a: 1 } | 1, { distributive: false }>, true>(true)
})

it('returns true for intersection type', () => {
// `object` intersect with any non-object type always returns `never`,
// and `object & object -> object` directly.
// so there is no intersection type that can produce a strict object.
testType.equal<IsNotStrictObject<object & Function>, true>(true)
testType.true<IsNotStrictObject<object & []>>(true)
})

it('works as filter', () => {
testType.equal<IsNotStrictObject<object, { selection: 'filter' }>, never>(true)
testType.equal<IsNotStrictObject<{ a: 1 }, { selection: 'filter' }>, { a: 1 }>(true)

testType.equal<IsNotStrictObject<never, { selection: 'filter' }>, never>(true)
testType.equal<IsNotStrictObject<unknown, { selection: 'filter' }>, unknown>(true)
testType.equal<IsNotStrictObject<object | boolean, { selection: 'filter' }>, boolean>(true)
testType.equal<IsNotStrictObject<{ a: 1 } | boolean, { selection: 'filter' }>, { a: 1 } | boolean>(true)
})

it('works with unique branches', () => {
testType.equal<IsNotStrictObject<object, IsNotStrictObject.$Branch>, $Else>(true)
testType.equal<IsNotStrictObject<{ a: 1 }, IsNotStrictObject.$Branch>, $Then>(true)

testType.equal<IsNotStrictObject<any, IsNotStrictObject.$Branch>, $Then>(true)
testType.equal<IsNotStrictObject<unknown, IsNotStrictObject.$Branch>, $Then>(true)
testType.equal<IsNotStrictObject<never, IsNotStrictObject.$Branch>, $Then>(true)
testType.equal<IsNotStrictObject<void, IsNotStrictObject.$Branch>, $Then>(true)

testType.equal<IsNotStrictObject<object | 1, IsNotStrictObject.$Branch>, $Then | $Else>(true)
})
34 changes: 34 additions & 0 deletions type-plus/ts/object/is_not_strict_object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { IdentityEqual } from '../equal/identity_equal.js'
import type { IsAnyOrNever } from '../mix_types/is_any_or_never.js'
import type { IsNever } from '../never/is_never.js'
import type { SelectInvertWithDistribute } from '../type_plus/branch/select_invert_with_distribute.js'
import type { $Else, $ResolveSelection, $SelectionBranch, $Then } from '../type_plus/branch/selection.js'
import type { $ResolveOptions } from '../type_plus/resolve_options.js'

/**
* 🎭 *predicate*
*
* Validate that `T` is strictly the `object` type.
*/
export type IsNotStrictObject<T, $O extends IsNotStrictObject.$Options = {}> =
IsAnyOrNever<T, $SelectionBranch> extends infer R
? R extends $Then ? $ResolveSelection<$O, T, $Then>
: R extends $Else ? ($ResolveOptions<[$O['distributive'], SelectInvertWithDistribute.$Default['distributive']]> extends true
? IsNotStrictObject._D<T, $O>
: SelectInvertWithDistribute._N<T, object, $O>)
: never : never

export namespace IsNotStrictObject {
export type $Options = SelectInvertWithDistribute.$Options
export type $Default = SelectInvertWithDistribute.$Default
export type $Branch = SelectInvertWithDistribute.$Branch
export type _D<T, $O extends IsNotStrictObject.$Options> =
T extends object
? IdentityEqual<T, {},
$ResolveSelection<$O, T, $Then>,
IsNever<keyof T, {
$then: $ResolveSelection<$O, T, $Else>,
$else: $ResolveSelection<$O, T, $Then>
}>>
: $ResolveSelection<$O, T, $Then>
}
44 changes: 32 additions & 12 deletions type-plus/ts/object/is_object.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { it } from '@jest/globals'
import { testType, type IsObject } from '../index.js'
import { testType, type IsObject, type $Then, type $Else } from '../index.js'

it('returns true if T is object', () => {
testType.true<IsObject<object>>(true)
Expand Down Expand Up @@ -43,20 +43,40 @@ it('returns false for all other types', () => {
testType.false<IsObject<1n>>(true)
})

it('returns false if T is union of object', () => {
testType.false<IsObject<object | 1>>(true)
it('distributes for union type', () => {
testType.equal<IsObject<object | 1>, boolean>(true)
testType.equal<IsObject<{ a: 1 } | 1>, boolean>(true)
})

it('returns true if T is intersection of object', () => {
testType.true<IsObject<object & { a: 1 }>>(true)
it('can disable union distribution', () => {
testType.equal<IsObject<{ a: 1 } | 1>, boolean>(true)
testType.equal<IsObject<{ a: 1 } | 1, { distributive: false }>, false>(true)
})

it('can override Then/Else', () => {
testType.equal<IsObject<object, 1, 2>, 1>(true)
testType.equal<IsObject<0, 1, 2>, 2>(true)
it('returns true for intersection type', () => {
testType.equal<object & [], object & []>(true)
testType.true<IsObject<object & []>>(true)
testType.true<IsObject<{ a: 1 } & []>>(true)
})

it('works as filter', () => {
testType.equal<IsObject<object, { selection: 'filter' }>, object>(true)
testType.equal<IsObject<{ a: 1 }, { selection: 'filter' }>, { a: 1 }>(true)

testType.equal<IsObject<never, { selection: 'filter' }>, never>(true)
testType.equal<IsObject<unknown, { selection: 'filter' }>, never>(true)
testType.equal<IsObject<object | boolean, { selection: 'filter' }>, object>(true)
testType.equal<IsObject<{ a: 1 } | boolean, { selection: 'filter' }>, { a: 1 }>(true)
})

it('works with unique branches', () => {
testType.equal<IsObject<object, IsObject.$Branch>, $Then>(true)
testType.equal<IsObject<{ a: 1 }, IsObject.$Branch>, $Then>(true)

testType.equal<IsObject<any, IsObject.$Branch>, $Else>(true)
testType.equal<IsObject<unknown, IsObject.$Branch>, $Else>(true)
testType.equal<IsObject<never, IsObject.$Branch>, $Else>(true)
testType.equal<IsObject<void, IsObject.$Branch>, $Else>(true)

testType.equal<IsObject<any, 1, 2>, 2>(true)
testType.equal<IsObject<unknown, 1, 2>, 2>(true)
testType.equal<IsObject<never, 1, 2>, 2>(true)
testType.equal<IsObject<void, 1, 2>, 2>(true)
testType.equal<IsObject<object | 1, IsObject.$Branch>, $Then | $Else>(true)
})
10 changes: 8 additions & 2 deletions type-plus/ts/object/is_object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ObjectType } from './object_type.js'
import type { SelectWithDistribute } from '../type_plus/branch/select_with_distribute.js'

/**
* Is `T` an `object`.
Expand All @@ -14,4 +14,10 @@ import type { ObjectType } from './object_type.js'
* ```
*/

export type IsObject<T, Then = true, Else = false> = ObjectType<T, Then, Else>
export type IsObject<T, $O extends IsObject.$Options = {}> = SelectWithDistribute<T, object, $O>

export namespace IsObject {
export type $Options = SelectWithDistribute.$Options
export type $Default = SelectWithDistribute.$Default
export type $Branch = SelectWithDistribute.$Branch
}
77 changes: 70 additions & 7 deletions type-plus/ts/object/is_strict_object.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { it } from '@jest/globals'
import { testType, type IsStrictObject } from '../index.js'
import { testType, type IsStrictObject, type $Then, type $Else } from '../index.js'


it('returns true for object', () => {
testType.equal<IsStrictObject<object>, true>(true)
})

it('returns false for object literal', () => {
testType.equal<IsStrictObject<{ a: number }>, false>(true)
})

it('returns false for empty object literal', () => {
testType.equal<IsStrictObject<{}>, false>(true)
})

it('returns false if T is array or tuple', () => {
testType.false<IsStrictObject<string[]>>(true)
testType.false<IsStrictObject<[]>>(true)
testType.false<IsStrictObject<[1, 2]>>(true)
})

it('returns false for special types', () => {
testType.equal<IsStrictObject<any>, false>(true)
Expand All @@ -8,14 +27,58 @@ it('returns false for special types', () => {
testType.equal<IsStrictObject<void>, false>(true)
})

it('returns false for object literal', () => {
testType.equal<IsStrictObject<{ a: number }>, false>(true)
it('returns false for all other types', () => {
testType.false<IsStrictObject<undefined>>(true)
testType.false<IsStrictObject<null>>(true)
testType.false<IsStrictObject<boolean>>(true)
testType.false<IsStrictObject<true>>(true)
testType.false<IsStrictObject<false>>(true)
testType.false<IsStrictObject<number>>(true)
testType.false<IsStrictObject<1>>(true)
testType.false<IsStrictObject<string>>(true)
testType.false<IsStrictObject<''>>(true)
testType.false<IsStrictObject<symbol>>(true)
testType.false<IsStrictObject<bigint>>(true)
testType.false<IsStrictObject<1n>>(true)
})

it('returns false for empty object literal', () => {
testType.equal<IsStrictObject<{}>, false>(true)
it('distributes for union type', () => {
testType.equal<IsStrictObject<object | 1>, boolean>(true)
testType.equal<IsStrictObject<object | boolean>, boolean>(true)
testType.equal<IsStrictObject<{ a: 1 } | 1>, false>(true)
})

it('returns true for object', () => {
testType.equal<IsStrictObject<object>, true>(true)
it('can disable union distribution', () => {
testType.equal<IsStrictObject<{ a: 1 } | 1>, false>(true)
testType.equal<IsStrictObject<{ a: 1 } | 1, { distributive: false }>, false>(true)
})

it('returns false for intersection type', () => {
// `object` intersect with any non-object type always returns `never`,
// and `object & object -> object` directly.
// so there is no intersection type that can produce a strict object.
testType.equal<IsStrictObject<object & Function>, false>(true)
testType.false<IsStrictObject<object & []>>(true)
})

it('works as filter', () => {
testType.equal<IsStrictObject<object, { selection: 'filter' }>, object>(true)
testType.equal<IsStrictObject<{ a: 1 }, { selection: 'filter' }>, never>(true)

testType.equal<IsStrictObject<never, { selection: 'filter' }>, never>(true)
testType.equal<IsStrictObject<unknown, { selection: 'filter' }>, never>(true)
testType.equal<IsStrictObject<object | boolean, { selection: 'filter' }>, object>(true)
testType.equal<IsStrictObject<{ a: 1 } | boolean, { selection: 'filter' }>, never>(true)
})

it('works with unique branches', () => {
testType.equal<IsStrictObject<object, IsStrictObject.$Branch>, $Then>(true)
testType.equal<IsStrictObject<{ a: 1 }, IsStrictObject.$Branch>, $Else>(true)

testType.equal<IsStrictObject<any, IsStrictObject.$Branch>, $Else>(true)
testType.equal<IsStrictObject<unknown, IsStrictObject.$Branch>, $Else>(true)
testType.equal<IsStrictObject<never, IsStrictObject.$Branch>, $Else>(true)
testType.equal<IsStrictObject<void, IsStrictObject.$Branch>, $Else>(true)

testType.equal<IsStrictObject<object | 1, IsStrictObject.$Branch>, $Then | $Else>(true)
})
Loading

0 comments on commit 4dc227a

Please sign in to comment.