Last active
November 21, 2021 19:49
-
-
Save webstrand/b0f79ef6ed37839d1432466fe8ddbc1a to your computer and use it in GitHub Desktop.
Functions and type aliases for unit testing compile-time types.
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
/** | |
* Type alias resolves to `True` if and only if `U` is the same as `V`, otherwise it resolves to `False`. | |
* @typeparam U - An arbitrary type | |
* @typeparam V - An arbitrary type | |
* @typeparam True - Production when `U` is the same as `V` | |
* @typeparam False - Production when `U` is not the same as `V` | |
*/ | |
export type Exact<U, V, True = true, False = false> = | |
{ <_>(): _ extends U ? 1 : 0 } extends { <_>(): _ extends V ? 1 : 0 } | |
? True | |
: False; | |
/** | |
* Type alias resolves to `true` if and only if `L` is the same as `R`. | |
* | |
* Left or right type does not matter, only affects error messages. | |
* @typeparam L - Left arbitrary type | |
* @typeparam R - Right arbitrary type | |
*/ | |
export type ExactTrue<L, R> = | |
0 extends (1 & L) | |
? 0 extends (1 & R) | |
? true | |
: "L is any but R is NOT any" & { L?: L, R?: R } | |
: 0 extends (1 & R) | |
? "L is NOT any but R is any" & { L?: L, R?: R } | |
: [L] extends [R] | |
? [R] extends [L] | |
? { <_>(): _ extends L ? 1 : 0 } extends { <_>(): _ extends R ? 1 : 0 } | |
? true | |
: "Variance between L and R" & { L?: L, R?: R } | |
: "R is not assignable to L" & { L?: L, R?: R } | |
: "L is not assignable to R" & { L?: L, R?: R }; | |
namespace TestExactTrue { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
declare function anyunknown(x: ExactTrue<any, unknown>): void; | |
anyunknown("L is any but R is NOT any"); | |
// @ts-expect-error | |
anyunknown(true); | |
declare function unknownany(x: ExactTrue<unknown, any>): void; | |
unknownany("L is NOT any but R is any"); | |
// @ts-expect-error | |
unknownany(true); | |
declare function anyany(x: ExactTrue<any, any>): void; | |
anyany(true); | |
declare function oneone(x: ExactTrue<1, 1>): void; | |
oneone(true); | |
declare function onetwo(x: ExactTrue<1, 2>): void; | |
onetwo("L is not assignable to R"); | |
// @ts-expect-error | |
onetwo(true); | |
declare function wideNarrow1(x: ExactTrue<{ foo: string }, { foo: "" }>): void; | |
wideNarrow1("L is not assignable to R"); | |
// @ts-expect-error | |
wideNarrow1(true); | |
declare function narrowWide1(x: ExactTrue<{ foo: "" }, { foo: string }>): void; | |
narrowWide1("R is not assignable to L"); | |
// @ts-expect-error | |
narrowWide1(true); | |
declare function wideNarrow2(x: ExactTrue<{ readonly foo: "" }, { foo: "" }>): void; | |
wideNarrow2("Variance between L and R"); | |
// @ts-expect-error | |
wideNarrow2(true); | |
declare function narrowWide2(x: ExactTrue<{ foo: "" }, { readonly foo: "" }>): void; | |
narrowWide2("Variance between L and R"); | |
// @ts-expect-error | |
narrowWide2(true); | |
type foo<N, Acc extends string[] = []> = Acc["length"] extends N ? Acc[number] : foo<N, [...Acc, `${Acc["length"]}`]>; | |
type bar<N, Acc extends string[] = []> = Acc["length"] extends N ? Acc[number] : bar<N, [`${Acc["length"]}`, ...Acc]>; | |
declare function foobar(x: ExactTrue<foo<30>, bar<30>>): void; | |
foobar(true); | |
declare function variance1(x: ExactTrue<[1,2?,3?], [1] | [1,2?,3?]>): void; | |
variance1("Variance between L and R"); | |
// @ts-expect-error | |
variance2(true); | |
declare function variance2(x: ExactTrue<{ foo: string, bar?: string }, { foo: string, bar?: string } | { foo: string }>): void; | |
variance2("Variance between L and R"); | |
// @ts-expect-error | |
variance2(true); | |
} | |
void TestExactTrue; | |
/** | |
* Type alias resolves to `false` if and only if `L` is not the same as `R`. | |
* | |
* Left or right type does not matter, only affects error messages. | |
* @typeparam L - Left arbitrary type | |
* @typeparam R - Right arbitrary type | |
*/ | |
export type ExactFalse<L, R> = | |
0 extends (1 & L) | |
? 0 extends (1 & R) | |
? "L is any and R is any" & { L?: L, R?: R } | |
: false | |
: 0 extends (1 & R) | |
? false | |
: [L] extends [R] | |
? [R] extends [L] | |
? { <_>(): _ extends L ? 1 : 0 } extends { <_>(): _ extends R ? 1 : 0 } | |
? "L is the same as R" & { L?: L, R?: R } | |
: false | |
: false | |
: false; | |
namespace TestExactFalse { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
declare function anyunknown(x: ExactFalse<any, unknown>): void; | |
anyunknown(false); | |
declare function unknownany(x: ExactFalse<unknown, any>): void; | |
unknownany(false); | |
declare function anyany(x: ExactFalse<any, any>): void; | |
anyany("L is any and R is any"); | |
// @ts-expect-error | |
anyany(false); | |
declare function oneone(x: ExactFalse<1, 1>): void; | |
oneone("L is the same as R"); | |
// @ts-expect-error | |
oneone(false) | |
declare function onetwo(x: ExactFalse<1, 2>): void; | |
onetwo(false); | |
declare function wideNarrow1(x: ExactFalse<{ foo: string }, { foo: "" }>): void; | |
wideNarrow1(false); | |
declare function narrowWide1(x: ExactFalse<{ foo: "" }, { foo: string }>): void; | |
narrowWide1(false); | |
declare function wideNarrow2(x: ExactFalse<{ readonly foo: "" }, { foo: "" }>): void; | |
wideNarrow2(false); | |
declare function narrowWide2(x: ExactFalse<{ foo: "" }, { readonly foo: "" }>): void; | |
narrowWide2(false); | |
type foo<N, Acc extends string[] = []> = Acc["length"] extends N ? Acc[number] : foo<N, [...Acc, `${Acc["length"]}`]>; | |
type bar<N, Acc extends string[] = []> = Acc["length"] extends N ? Acc[number] : bar<N, [`${Acc["length"]}`, ...Acc]>; | |
declare function foobar(x: ExactFalse<foo<30>, bar<30>>): void; | |
foobar("L is the same as R"); | |
declare function variance1(x: ExactFalse<[1,2?,3?], [1] | [1,2?,3?]>): void; | |
variance2(false); | |
declare function variance2(x: ExactFalse<{ foo: string, bar?: string }, { foo: string, bar?: string } | { foo: string }>): void; | |
variance2(false); | |
} | |
void TestExactFalse; | |
/** Unique symbol to use as placeholder on inferrence failure */ | |
const Unspecified = Symbol(); | |
/** Unique symbol to use as placeholder on inferrence failure */ | |
type Unspecified = typeof Unspecified; | |
/** | |
* Utility type produces an error message when inferrence fails and resolves to | |
* `Unspecified` | |
*/ | |
type Spec<Name extends "L" | "R", LR, Return> = | |
Unspecified extends LR | |
? `type parameter ${Name} must be specified or inferred` | |
: Return; | |
/** | |
* Compare `L` and `R` for equivalence, partial application for partial inferrence. | |
* @param v - Optionally specify `l` to infer `L`. | |
* @template L - May be specified manually, or inferred from function parameter `l` | |
*/ | |
export function Exact<L = Unspecified>(l?: L): Spec<"L", L, { | |
/** | |
* @param v - `true` when `L` is the same as `R` | |
* @template R - Must be specified, use a function parameter to infer | |
*/ | |
<R = Unspecified>(v: Spec<"L", L, ExactTrue<L, R>>): void; | |
/** | |
* @param r - infers `R` from the function parameter | |
* @param v - `true` when `L` is the same as `R` | |
* @template R - inferred from function parameter `r` | |
*/ | |
<R>(r: R, v: ExactTrue<L, R>): void; | |
/** | |
* @param v - `false` when `L` is not the same as `R` | |
* @template R - Must be specified, use a function parameter to infer | |
*/ | |
<R = Unspecified>(v: Spec<"R", R, ExactFalse<L, R>>): void; | |
/** | |
* @param r - infers `R` from the function parameter | |
* @param v - `false` when `L` is not the same as `R` | |
* @template R - inferred from function parameter `r` | |
*/ | |
<R>(r: R, v: ExactFalse<L, R>): void; | |
}>; | |
/** | |
* Compare `L` and `R` for equivalence, manually bind type parameter `L` and | |
* `R`. | |
* @param v - `true` when `L` is the same as `R` | |
* @template L - Must be specified, use a function parameter to infer | |
* @template R - Must be specified, use a function parameter to infer | |
*/ | |
export function Exact<L = Unspecified, R = Unspecified>( | |
v: Spec<"L", L, Spec<"R", R, ExactTrue<L, R>>> | |
): void; | |
/** | |
* Compare `L` and `R` for equivalence, manually bind type parameter `L` and | |
* `R`. | |
* @param v - `false` when `L` is not the same as `R` | |
* @template L - Must be specified, use a function parameter to infer | |
* @template R - Must be specified, use a function parameter to infer | |
*/ | |
export function Exact<L = Unspecified, R = Unspecified>( | |
v: Spec<"L", L, Spec<"R", R, ExactFalse<L, R>>> | |
): void; | |
/** | |
* Compare `L` and `R` for equivalence, infers type parameter `L` and `R`. | |
* @param l - infers `L` from the function parameter | |
* @param r - infers `R` from the function parameter | |
* @param v - `true` when `L` is the same as `R` | |
* @template L - inferred from function parameter `l` | |
* @template R - inferred from function parameter `r` | |
*/ | |
export function Exact<L, R>(l: L, r: R, v: ExactTrue<L, R>): void; | |
/** | |
* Compare `L` and `R` for equivalence, infers type parameter `L` and `R`. | |
* @param l - infers `L` from the function parameter | |
* @param r - infers `R` from the function parameter | |
* @param v - `false` when `L` is not the same as `R` | |
* @template L - inferred from function parameter `l` | |
* @template R - inferred from function parameter `r` | |
*/ | |
export function Exact<L, R>(l: L, r: R, v: ExactFalse<L, R>): void; | |
/** @private stub implementation */ | |
export function Exact(): any { | |
return Exact; | |
} | |
// As far as TS knows, Exact() sometimes resolves to a string. Prevent | |
// misuse of Exact as a string at runtime. | |
Object.defineProperties(Exact, { | |
[Symbol.toPrimitive]: { value() { throw "Exact may not be coerced" }, enumerable: false }, | |
valueOf: { value() { throw "Exact may not be coerced" }, enumerable: false } | |
}); | |
namespace TestExactSpecificLCurried { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
// If the user doesn't specify L or R when not inferred, report an error. | |
const e1: "type parameter L must be specified or inferred" = Exact(); | |
Exact<1>()("type parameter R must be specified or inferred"); | |
{ // Test equivalence paths | |
Exact<1>()<1>(true); | |
// @ts-expect-error | |
Exact<1>()<1>(false); | |
Exact<1>()<1>("L is the same as R"); | |
Exact<1>()(1 as const, true); | |
// @ts-expect-error | |
Exact<1>()(1 as const, false); | |
Exact<1>()(1 as const, "L is the same as R"); | |
} | |
{ // Test inequivalence paths | |
Exact<1>()<2>(false); | |
// @ts-expect-error | |
Exact<1>()<2>(true); | |
Exact<1>()<2>("L is not assignable to R"); | |
Exact<1>()(2 as const, false); | |
// @ts-expect-error | |
Exact<1>()(2 as const, true); | |
Exact<1>()(2 as const, "L is not assignable to R"); | |
} | |
{ // Test partial equivalence, left priority | |
Exact<{ foo: string }>()<{ foo: "" }>(false); | |
// @ts-expect-error | |
Exact<{ foo: string }>()<{ foo: "" }>(true); | |
Exact<{ foo: string }>()<{ foo: "" }>("L is not assignable to R"); | |
} | |
{ // Test partial equivalence, right priority | |
Exact<{ foo: "" }>()<{ foo: string }>(false); | |
// @ts-expect-error | |
Exact<{ foo: "" }>()<{ foo: string }>(true); | |
Exact<{ foo: "" }>()<{ foo: string }>("R is not assignable to L"); | |
} | |
} | |
void TestExactSpecificLCurried; | |
namespace TestExactInferredLCurried { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
// If the user doesn't specify R when not inferred, report an error. | |
Exact(1 as const)("type parameter R must be specified or inferred"); | |
// Ensure that undefined is inferred correctly, due to optional parameter l. | |
// If inferrence fails, Exact(undefined) is not callable. | |
Exact(undefined)(0 as any); | |
{ // Test equivalence paths | |
Exact(1 as const)<1>(true); | |
// @ts-expect-error | |
Exact(1 as const)<1>(false); | |
Exact(1 as const)<1>("L is the same as R"); | |
Exact(1 as const)(1 as const, true); | |
// @ts-expect-error | |
Exact(1 as const)(1 as const, false); | |
Exact(1 as const)(1 as const, "L is the same as R"); | |
} | |
{ // Test inequivalence paths | |
Exact(1 as const)<2>(false); | |
// @ts-expect-error | |
Exact(1 as const)<2>(true); | |
Exact(1 as const)<2>("L is not assignable to R"); | |
Exact(1 as const)(2 as const, false); | |
// @ts-expect-error | |
Exact(1 as const)(2 as const, true); | |
Exact(1 as const)(2 as const, "L is not assignable to R"); | |
} | |
{ // Test partial equivalence, left priority | |
Exact({ foo: "" })<{ foo: "" }>(false); | |
// @ts-expect-error | |
Exact({ foo: "" })<{ foo: "" }>(true); | |
Exact({ foo: "" })<{ foo: "" }>("L is not assignable to R"); | |
} | |
{ // Test partial equivalence, right priority | |
Exact({ foo: "" as const })<{ foo: string }>(false); | |
// @ts-expect-error | |
Exact({ foo: "" as const })<{ foo: string }>(true); | |
Exact({ foo: "" as const })<{ foo: string }>("R is not assignable to L"); | |
} | |
} | |
void TestExactInferredLCurried; | |
namespace TestExactSpecificLSpecificR { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
// If the user doesn't specify L or R when not inferred, report an error. | |
Exact<Unspecified>("type parameter L must be specified or inferred"); | |
Exact<1>("type parameter R must be specified or inferred"); | |
{ // Test equivalence paths | |
Exact<1, 1>(true); | |
// @ts-expect-error | |
Exact<1, 1>(false); | |
Exact<1, 1>("L is the same as R"); | |
} | |
{ // Test inequivalence paths | |
Exact<1, 2>(false); | |
// @ts-expect-error | |
Exact<1, 2>(true); | |
Exact<1, 2>("L is not assignable to R"); | |
} | |
{ // Test partial equivalence, left priority | |
Exact<{ foo: string }, { foo: "" }>(false); | |
// @ts-expect-error | |
Exact<{ foo: string }, { foo: "" }>(true); | |
Exact<{ foo: string }, { foo: "" }>("L is not assignable to R"); | |
} | |
{ // Test partial equivalence, right priority | |
Exact<{ foo: "" }, { foo: string }>(false); | |
// @ts-expect-error | |
Exact<{ foo: "" }, { foo: string }>(true); | |
Exact<{ foo: "" }, { foo: string }>("R is not assignable to L"); | |
} | |
} | |
void TestExactSpecificLSpecificR; | |
namespace TestExactInferLInferR { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
{ // Test equivalence paths | |
Exact(1 as const, 1 as const, true); | |
// @ts-expect-error | |
Exact(1 as const, 1 as const, false); | |
Exact(1 as const, 1 as const, "L is the same as R"); | |
} | |
{ // Test inequivalence paths | |
Exact(1 as const, 2 as const, false); | |
// @ts-expect-error | |
Exact(1 as const, 2 as const, true); | |
Exact(1 as const, 2 as const, "L is not assignable to R"); | |
} | |
{ // Test partial equivalence, left priority | |
Exact({ foo: "" }, { foo: "" as const }, false); | |
// @ts-expect-error | |
Exact({ foo: "" }, { foo: "" as const }, true); | |
Exact({ foo: "" }, { foo: "" as const }, "L is not assignable to R"); | |
} | |
{ // Test partial equivalence, right priority | |
Exact({ foo: "" as const }, { foo: "" }, false); | |
// @ts-expect-error | |
Exact({ foo: "" as const }, { foo: "" }, true); | |
Exact({ foo: "" as const }, { foo: "" }, "R is not assignable to L"); | |
} | |
} | |
void TestExactInferLInferR; | |
/** | |
* Compare `L` and `R` for assignability from `L` onto `R`. | |
* @param l - Optionally specify `l` to infer `L`. | |
* @template L - Source type, assigned to R. | |
*/ | |
export function Assignable<L = Unspecified>(l?: L): Spec<"L", L, { | |
/** | |
* @param v - `true` when `L` is assignable to `R`, false otherwise. | |
* @template R - Must be specified, use a function parameter to infer | |
*/ | |
<R = Unspecified>(v: Spec<"R", R, [L] extends [R] ? true : false>): void; | |
/** | |
* @param r - infers `R` from the function parameter | |
* @param v - `true` when `L` is assignable to `R`, false otherwise. | |
*/ | |
<R>(r: R, v: [L] extends [R] ? true : false): void; | |
}> | |
/** | |
* Compare `L` and `R` for assignability from `L` onto `R`, manually bind type | |
* parameter `L` and `R`. | |
* @param v - `true` when L is assignable to R. | |
* @template L - Source type, assigned to R. | |
* @template R - Destination type, assigned from L. | |
*/ | |
export function Assignable<L = Unspecified, R = Unspecified>( | |
v: Spec<"L", L, Spec<"R", R, [L] extends [R] ? true : false>> | |
): void; | |
/** | |
* Compare `L` and `R` for assignability from `L` onto `R`, inferrs type | |
* parameter `L` and `R`. | |
* @param v - `true` when L is assignable to R. | |
* @param r - infers `R` from the function parameter | |
* @template L - Source type, assigned to R. | |
* @template R - Destination type, assigned from L. | |
*/ | |
export function Assignable<L, R>( | |
l: L, | |
r: R, | |
v: [L] extends [R] ? true : false | |
): void; | |
/** @private stub implementation */ | |
export function Assignable(): any { | |
return Assignable; | |
} | |
// As far as TS knows, Assignable() sometimes resolves to a string. Prevent | |
// misuse of Assignable as a string at runtime. | |
Object.defineProperties(Assignable, { | |
[Symbol.toPrimitive]: { | |
value() { throw "Assignable may not be coerced" }, | |
enumerable: false | |
}, | |
valueOf: { | |
value() { throw "Assignable may not be coerced" }, | |
enumerable: false | |
} | |
}); | |
namespace TestAssignableSpecificLCurried { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
Assignable<1>()<1>(true); | |
Assignable<1>()<2>(false); | |
Assignable<1>()<number>(true); | |
Assignable<1>()<1|2>(true); | |
Assignable<number>()<1|2>(false); | |
Assignable<1>()(1 as const, true); | |
Assignable<1>()(2 as const, false); | |
Assignable<1>()(2, true); | |
Assignable<1>()(1 as 1 | 2, true); | |
Assignable<number>()(1 as 1 | 2, false); | |
} | |
void TestAssignableSpecificLCurried; | |
namespace TestAssignableInferLCurried { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
Assignable(1 as const)<1>(true); | |
Assignable(1 as const)<2>(false); | |
Assignable(1 as const)<number>(true); | |
Assignable(1 as const)<1|2>(true); | |
Assignable(1)<1|2>(false); | |
Assignable(1 as const)(1 as const, true); | |
Assignable(1 as const)(2 as const, false); | |
Assignable(1 as const)(2, true); | |
Assignable(1 as const)(1 as 1 | 2, true); | |
Assignable(1)(1 as 1 | 2, false); | |
} | |
void TestAssignableInferLCurried; | |
namespace TestAssignableSpecificLSpecificR { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
Assignable<1, 1>(true); | |
Assignable<1, 2>(false); | |
Assignable<1, number>(true); | |
Assignable<1, 1|2>(true); | |
Assignable<number, 1|2>(false); | |
} | |
void TestAssignableSpecificLSpecificR; | |
namespace TestAssignableInferLInferR { | |
// @ts-ignore - prevent namespace from executing | |
if(1 as 0) return; | |
Assignable(1 as const, 1 as const, true); | |
Assignable(1 as const, 2 as const, false); | |
Assignable(1 as const, 2, true); | |
Assignable(1 as const, 1 as 1 | 2, true); | |
Assignable(1, 1 as 1 | 2, false); | |
} | |
void TestAssignableInferLInferR; | |
// from https://gist.github.com/webstrand/b0f79ef6ed37839d1432466fe8ddbc1a |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment