-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <mmkal@users.noreply.github.com>
- Loading branch information
Showing
4 changed files
with
195 additions
and
187 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
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>> |
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,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> |
Oops, something went wrong.