Skip to content

Commit

Permalink
branding.ts and messages.ts (#95)
Browse files Browse the repository at this point in the history
Another intermediate PR in support of #83

Moves `DeepBrand` to its own file, because in #83 I want to start using
imports from `overloads.ts` in it. This would be a circular reference,
which TypeScript can actually handle, but I would still like to avoid to
prevent confusion.

Similarly I had to create _another_ file `messages.ts` since we have a
few type-error aiding utilities that are currently in utils.ts, but that
rely on `DeepBrand`.

This tends to be the kind of file-proliferation that I worry about when
breaking up a mega-file! But in this case I think it's warranted.

@aryaemami59 what do you think re naming/ the new structure?

---------

Co-authored-by: Misha Kaletsky <[email protected]>
  • Loading branch information
mmkal and mmkal authored Aug 11, 2024
1 parent 14e715c commit 2108a4c
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 187 deletions.
75 changes: 75 additions & 0 deletions src/branding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
IsNever,
IsAny,
IsUnknown,
ReadonlyKeys,
RequiredKeys,
OptionalKeys,
MutuallyExtends,
ConstructorParams,
} from './utils'

/**
* Represents a deeply branded type.
*
* Recursively walk a type and replace it with a branded type related to the
* original. This is useful for equality-checking stricter than
* `A extends B ? B extends A ? true : false : false`, because it detects the
* difference between a few edge-case types that vanilla TypeScript
* doesn't by default:
* - `any` vs `unknown`
* - `{ readonly a: string }` vs `{ a: string }`
* - `{ a?: string }` vs `{ a: string | undefined }`
*
* __Note__: not very performant for complex types - this should only be used
* when you know you need it. If doing an equality check, it's almost always
* better to use {@linkcode StrictEqualUsingTSInternalIdenticalToOperator}.
*/
export type DeepBrand<T> =
IsNever<T> extends true
? {type: 'never'}
: IsAny<T> extends true
? {type: 'any'}
: IsUnknown<T> extends true
? {type: 'unknown'}
: T extends string | number | boolean | symbol | bigint | null | undefined | void
? {
type: 'primitive'
value: T
}
: T extends new (...args: any[]) => any
? {
type: 'constructor'
params: ConstructorParams<T>
instance: DeepBrand<InstanceType<Extract<T, new (...args: any) => any>>>
}
: T extends (...args: infer P) => infer R // avoid functions with different params/return values matching
? {
type: 'function'
params: DeepBrand<P>
return: DeepBrand<R>
this: DeepBrand<ThisParameterType<T>>
props: DeepBrand<Omit<T, keyof Function>>
}
: T extends any[]
? {
type: 'array'
items: {
[K in keyof T]: T[K]
}
}
: {
type: 'object'
properties: {
[K in keyof T]: DeepBrand<T[K]>
}
readonly: ReadonlyKeys<T>
required: RequiredKeys<T>
optional: OptionalKeys<T>
constructorParams: DeepBrand<ConstructorParams<T>>
}

/**
* Checks if two types are strictly equal using branding.
*/
export type StrictEqualUsingBranding<Left, Right> = MutuallyExtends<DeepBrand<Left>, DeepBrand<Right>>
14 changes: 9 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {StrictEqualUsingBranding} from './branding'
import {
StrictEqualUsingTSInternalIdenticalToOperator,
MismatchInfo,
AValue,
MismatchArgs,
Extends,
StrictEqualUsingBranding,
Scolder,
ExpectAny,
ExpectUnknown,
Expand All @@ -20,11 +16,19 @@ import {
ExpectNull,
ExpectUndefined,
ExpectNullable,
} from './messages'
import {
StrictEqualUsingTSInternalIdenticalToOperator,
AValue,
MismatchArgs,
Extends,
Params,
ConstructorParams,
} from './utils'

export * from './branding' // backcompat, consider removing in next major version
export * from './utils' // backcompat, consider removing in next major version
export * from './messages' // backcompat, consider removing in next major version

/**
* Represents the positive assertion methods available for type checking in the
Expand Down
108 changes: 108 additions & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {StrictEqualUsingBranding} from './branding'
import {And, Extends, Not, IsAny, UsefulKeys, ExtendsExcludingAnyOrNever, IsUnknown, IsNever} from './utils'

/**
* Determines the printable type representation for a given type.
*/
export type PrintType<T> =
IsUnknown<T> extends true
? 'unknown'
: IsNever<T> extends true
? 'never'
: IsAny<T> extends true
? never // special case, can't use `'any'` because that would match `any`
: boolean extends T
? 'boolean'
: T extends boolean
? `literal boolean: ${T}`
: string extends T
? 'string'
: T extends string
? `literal string: ${T}`
: number extends T
? 'number'
: T extends number
? `literal number: ${T}`
: T extends null
? 'null'
: T extends undefined
? 'undefined'
: T extends (...args: any[]) => any
? 'function'
: '...'

/**
* Helper for showing end-user a hint why their type assertion is failing.
* This swaps "leaf" types with a literal message about what the actual and expected types are.
* Needs to check for Not<IsAny<Actual>> because otherwise LeafTypeOf<Actual> returns never, which extends everything 🤔
*/
export type MismatchInfo<Actual, Expected> =
And<[Extends<PrintType<Actual>, '...'>, Not<IsAny<Actual>>]> extends true
? And<[Extends<any[], Actual>, Extends<any[], Expected>]> extends true
? Array<MismatchInfo<Extract<Actual, any[]>[number], Extract<Expected, any[]>[number]>>
: {
[K in UsefulKeys<Actual> | UsefulKeys<Expected>]: MismatchInfo<
K extends keyof Actual ? Actual[K] : never,
K extends keyof Expected ? Expected[K] : never
>
}
: StrictEqualUsingBranding<Actual, Expected> extends true
? Actual
: `Expected: ${PrintType<Expected>}, Actual: ${PrintType<Exclude<Actual, Expected>>}`

const inverted = Symbol('inverted')
type Inverted<T> = {[inverted]: T}

const expectNull = Symbol('expectNull')
export type ExpectNull<T> = {[expectNull]: T; result: ExtendsExcludingAnyOrNever<T, null>}

const expectUndefined = Symbol('expectUndefined')
export type ExpectUndefined<T> = {[expectUndefined]: T; result: ExtendsExcludingAnyOrNever<T, undefined>}

const expectNumber = Symbol('expectNumber')
export type ExpectNumber<T> = {[expectNumber]: T; result: ExtendsExcludingAnyOrNever<T, number>}

const expectString = Symbol('expectString')
export type ExpectString<T> = {[expectString]: T; result: ExtendsExcludingAnyOrNever<T, string>}

const expectBoolean = Symbol('expectBoolean')
export type ExpectBoolean<T> = {[expectBoolean]: T; result: ExtendsExcludingAnyOrNever<T, boolean>}

const expectVoid = Symbol('expectVoid')
export type ExpectVoid<T> = {[expectVoid]: T; result: ExtendsExcludingAnyOrNever<T, void>}

const expectFunction = Symbol('expectFunction')
export type ExpectFunction<T> = {[expectFunction]: T; result: ExtendsExcludingAnyOrNever<T, (...args: any[]) => any>}

const expectObject = Symbol('expectObject')
export type ExpectObject<T> = {[expectObject]: T; result: ExtendsExcludingAnyOrNever<T, object>}

const expectArray = Symbol('expectArray')
export type ExpectArray<T> = {[expectArray]: T; result: ExtendsExcludingAnyOrNever<T, any[]>}

const expectSymbol = Symbol('expectSymbol')
export type ExpectSymbol<T> = {[expectSymbol]: T; result: ExtendsExcludingAnyOrNever<T, symbol>}

const expectAny = Symbol('expectAny')
export type ExpectAny<T> = {[expectAny]: T; result: IsAny<T>}

const expectUnknown = Symbol('expectUnknown')
export type ExpectUnknown<T> = {[expectUnknown]: T; result: IsUnknown<T>}

const expectNever = Symbol('expectNever')
export type ExpectNever<T> = {[expectNever]: T; result: IsNever<T>}

const expectNullable = Symbol('expectNullable')
export type ExpectNullable<T> = {[expectNullable]: T; result: Not<StrictEqualUsingBranding<T, NonNullable<T>>>}

/**
* Checks if the result of an expecter matches the specified options, and resolves to a fairly readable error messsage if not.
*/
export type Scolder<
Expecter extends {result: boolean},
Options extends {positive: boolean},
> = Expecter['result'] extends Options['positive']
? () => true
: Options['positive'] extends true
? Expecter
: Inverted<Expecter>
Loading

0 comments on commit 2108a4c

Please sign in to comment.