Skip to content

Commit

Permalink
feat: add Assignable
Browse files Browse the repository at this point in the history
  • Loading branch information
unional committed Oct 23, 2023
1 parent d957b78 commit 4c991f1
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 14 deletions.
6 changes: 6 additions & 0 deletions .changeset/serious-suns-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"type-plus": minor
---

Add `Assignable<A, B>`.
Deprecated `CanAssign<A, B>` and `StrictCanAssign<A, B>`.
3 changes: 2 additions & 1 deletion type-plus/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ 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 * from './predicates/assignable.js'
export * from './predicates/index.js'
export type { PrimitiveTypes } from './primitive.js'
export * from './promise/index.js'
Expand Down Expand Up @@ -121,8 +122,8 @@ export type * from './type_plus/branch/$input_options.js'
export type * from './type_plus/branch/$is_distributive.js'
export type * from './type_plus/branch/$resolve_branch.js'
export type * from './type_plus/branch/$select.js'
export type * from './type_plus/branch/$selection_options.js'
export type * from './type_plus/branch/$selection.js'
export type * from './type_plus/branch/$selection_options.js'
export type * from './undefined/has_undefined.js'
export type * from './undefined/is_not_undefined.js'
export type * from './undefined/is_undefined.js'
Expand Down
18 changes: 6 additions & 12 deletions type-plus/ts/predicates/CanAssign.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { NotExtendable } from './Extends.js'
import type { IsEmptyObject } from './IsEmptyObject.js'
import type { Assignable } from './assignable.js'

/**
* Can `A` assign to `B`
Expand All @@ -10,6 +10,8 @@ import type { IsEmptyObject } from './IsEmptyObject.js'
*
* This is the correct behavior.
*
* @deprecated use `Assignable<A, B>` instead
*
* @example
* ```ts
* CanAssign<number | string, number> // boolean
Expand Down Expand Up @@ -37,23 +39,15 @@ export type CanAssign<A, B, Then = true, Else = false> = boolean extends A
*
* All branches in an union `A` are assignable to `B`.
*
* @deprecated use `Assignable<A, B` instead
*
* @example
* ```ts
* StrictCanAssign<number | string, number> // false
* StrictCanAssign<number | string, number | string> // true
* ```
*/
export type StrictCanAssign<A, B, Then = true, Else = false> = IsEmptyObject<A> extends true
? Record<string, unknown> extends B
? Then
: Else
: boolean extends A
? boolean extends B
? Then
: Else
: [A] extends [B]
? Then
: Else
export type StrictCanAssign<A, B, Then = true, Else = false> = Assignable<A, B, { distributive: false, $then: Then, $else: Else }>

/**
* @deprecated use `CanAssign` instead
Expand Down
3 changes: 3 additions & 0 deletions type-plus/ts/predicates/Extends.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @deprecated use `$Assignable`
*/
export type Extendable<A, B, Then = A, Else = never> = A extends B ? Then : Else
export type NotExtendable<A, B, Then = A, Else = never> = A extends B ? Else : Then
export type IsExtend<A, B, Then = true, Else = false> = A extends B ? Then : Else
Expand Down
113 changes: 113 additions & 0 deletions type-plus/ts/predicates/assignable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { it } from '@jest/globals'
import { testType, type Assignable, type $Then, type $Else } from '../index.js'

it('check if A can be assigned to B', () => {
testType.true<Assignable<1, 1>>(true)
testType.true<Assignable<1, number>>(true)
testType.true<Assignable<number, number>>(true)
testType.true<Assignable<'a', 'a'>>(true)
testType.true<Assignable<'a', string>>(true)
testType.true<Assignable<string, string>>(true)
testType.true<Assignable<false, boolean>>(true)
testType.true<Assignable<true, boolean>>(true)
testType.true<Assignable<boolean, boolean>>(true)
testType.true<Assignable<{ a: 1 }, { a: number }>>(true)
testType.true<Assignable<{ a: string, b: number }, { a: string }>>(true)

testType.false<Assignable<number, 1>>(true)
testType.false<Assignable<string, 'a'>>(true)
testType.false<Assignable<{ a: number }, { a: 1 }>>(true)
testType.false<Assignable<{ a: string }, { a: string, b: number }>>(true)
})

it('returns true when B is `any` as anything can be assigned to `any`', () => {
testType.true<Assignable<any, any>>(true)
testType.true<Assignable<unknown, any>>(true)
testType.true<Assignable<never, any>>(true)

testType.true<Assignable<1, any>>(true)
testType.true<Assignable<null, any>>(true)
testType.true<Assignable<undefined, any>>(true)
})

it('returns true when B is `unknown` as anything can be assigned to `unknown`', () => {
testType.true<Assignable<any, unknown>>(true)
testType.true<Assignable<unknown, unknown>>(true)
testType.true<Assignable<never, unknown>>(true)

testType.true<Assignable<1, unknown>>(true)
testType.true<Assignable<null, unknown>>(true)
testType.true<Assignable<undefined, unknown>>(true)
})

it('returns false when B is `never` except when A is `never`', () => {
testType.false<Assignable<any, never>>(true)
testType.false<Assignable<unknown, never>>(true)
testType.true<Assignable<never, never>>(true)

testType.false<Assignable<1, never>>(true)
testType.false<Assignable<null, never>>(true)
testType.false<Assignable<undefined, never>>(true)
})

it('works against special types', () => {
testType.equal<Assignable<any, 1>, true>(true)
testType.equal<Assignable<unknown, 1>, true>(true)
testType.equal<Assignable<never, 1>, true>(true)
})

it('can disable distribution', () => {
testType.equal<Assignable<boolean, true>, boolean>(true)
testType.equal<Assignable<boolean, true, { distributive: false }>, false>(true)

testType.equal<Assignable<number | string, number>, boolean>(true)
testType.equal<Assignable<number | string, number, { distributive: false }>, false>(true)
})

it('can use as filter', () => {
testType.equal<Assignable<1, number, { selection: 'filter' }>, 1>(true)
testType.never<Assignable<number, 1, { selection: 'filter' }>>(true)
})

it('work as branching', () => {
testType.equal<Assignable<1, any, Assignable.$Branch>, $Then>(true)
testType.equal<Assignable<1, unknown, Assignable.$Branch>, $Then>(true)
testType.equal<Assignable<1, never, Assignable.$Branch>, $Else>(true)
testType.equal<Assignable<never, never, Assignable.$Branch>, $Then>(true)
testType.equal<Assignable<1, number, Assignable.$Branch>, $Then>(true)
testType.equal<Assignable<true, number, Assignable.$Branch>, $Else>(true)
})

it('works with partial customization', () => {
testType.equal<Assignable<any, any, { $then: 1 }>, 1>(true)
testType.equal<Assignable<any, unknown, { $then: 1 }>, 1>(true)
testType.equal<Assignable<never, never, { $then: 1 }>, 1>(true)
testType.equal<Assignable<0, number, { $then: 1 }>, 1>(true)
testType.equal<Assignable<1, never, { $else: 2 }>, 2>(true)
testType.equal<Assignable<0, string, { $else: 2 }>, 2>(true)

testType.equal<Assignable<any, any, { $else: 2 }>, true>(true)
testType.equal<Assignable<any, unknown, { $else: 1 }>, true>(true)
testType.equal<Assignable<never, never, { $else: 1 }>, true>(true)
testType.equal<Assignable<0, number, { $else: 1 }>, true>(true)
testType.equal<Assignable<1, never, { $then: 2 }>, false>(true)
testType.equal<Assignable<0, string, { $then: 2 }>, false>(true)
})

it('can override $any branch', () => {
testType.equal<Assignable<any, any>, true>(true)
testType.equal<Assignable<any, any, { $any: unknown }>, unknown>(true)
testType.equal<Assignable<any, number, { $any: unknown }>, unknown>(true)
})

it('can override $unknown branch', () => {
testType.equal<Assignable<unknown, unknown>, true>(true)
testType.equal<Assignable<unknown, unknown, { $unknown: unknown }>, unknown>(true)
testType.equal<Assignable<unknown, number, { $unknown: unknown }>, unknown>(true)
})

it('can override $never branch', () => {
testType.equal<Assignable<never, never>, true>(true)
testType.equal<Assignable<never, never, { $never: unknown }>, unknown>(true)
testType.equal<Assignable<never, number, { $never: unknown }>, unknown>(true)
})
94 changes: 94 additions & 0 deletions type-plus/ts/predicates/assignable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { $Any } from '../any/any.js'
import type { $Never } from '../never/never.js'
import type { $SpecialType } from '../type_plus/$special_type.js'
import type { $DistributiveDefault, $DistributiveOptions } from '../type_plus/branch/$distributive.js'
import type { $InputOptions } from '../type_plus/branch/$input_options.js'
import type { $IsDistributive } from '../type_plus/branch/$is_distributive.js'
import type { $ResolveBranch } from '../type_plus/branch/$resolve_branch.js'
import type { $Else, $SelectionBranch, $SelectionPredicate, $Then } from '../type_plus/branch/$selection.js'
import type { $SelectionOptions } from '../type_plus/branch/$selection_options.js'
import type { $Unknown } from '../unknown/unknown.js'

/**
* 🧰 *tool utils*
*
* Validate if `A` is assignable to `B`.
*
* @example
* ```ts
* type R = Assignable<any, any> // true
* type R = Assignable<any, 1> // true
* type R = Assignable<unknown, unknown> // true
* type R = Assignable<never, never> // true
* type R = Assignable<1, 1> // true
* type R = Assignable<'a', 'a'> // true
* type R = Assignable<'a', 'b'> // false
* type R = Assignable<'a', string> // true
* ```
*
* 🔢 *customize*
*
* Filter to ensure `A` is assignable to `B`.
*
* @example
* ```ts
* type R = Assignable<any, any, { selection: 'filter' }> // any
* type R = Assignable<1, number, { selection: 'filter' }> // 1
* ```
*
* 🔢 *customize*
*
* Use unique branch identifiers to allow precise processing of the result.
*
* @example
* ```ts
* type R = Assignable<any, any, Assignable.$Branch> // $Then
* ```
*
* 🔢 *customize*
*
* Override special types branch.
*
* @example
* ```ts
* type R = Assignable<any, any, { $any: 1 }> // 1
* type R = Assignable<unknown, any, { $unknown: 1 }> // 1
* type R = Assignable<never, any, { $never: 1 }> // 1
* ```
*/
export type Assignable<
A,
B,
$O extends Assignable.$Options = {}
> = $SpecialType<B, {
$any: $ResolveBranch<A, $O, [0 extends 1 & A ? $Any : unknown, $Then]>,
$unknown: $ResolveBranch<A, $O, [[A, unknown] extends [unknown, A] ? $Unknown : unknown, $Then]>,
$never: $ResolveBranch<A, $O, [$Never, [A, never] extends [never, A] ? $Then : $Else]>,
$else: $SpecialType<A, {
$any: $ResolveBranch<A, $O, [$Any, $Then]>,
$unknown: $ResolveBranch<A, $O, [$Unknown, $Then]>,
$never: $ResolveBranch<A, $O, [$Never, $Then]>,
$else: Assignable.$<A, B, $O>
}>
}>


export namespace Assignable {
export type $Options = $SelectionOptions & $DistributiveOptions & $InputOptions<$Any | $Unknown | $Never>
export type $Default = $SelectionPredicate & $DistributiveDefault
export type $Branch = $SelectionBranch & $DistributiveDefault

/**
* 🧰 *type util*
*
*
*/
export type $<
A,
B,
$O extends Assignable.$Options = {}
> = $IsDistributive<$O, {
$then: $ResolveBranch<A, $O, [A extends B ? $Then : $Else]>,
$else: $ResolveBranch<A, $O, [[A] extends [B] ? $Then : $Else]>
}>
}
1 change: 0 additions & 1 deletion type-plus/ts/predicates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ export type { If } from './If.js'
export type { IsEmptyObject } from './IsEmptyObject.js'
export type { IsLiteral } from './literal.js'
export type { And, Not, Or, Xor } from './logical.js'

0 comments on commit 4c991f1

Please sign in to comment.