Skip to content

Instantly share code, notes, and snippets.

@webstrand
Last active November 21, 2021 19:49
Show Gist options
  • Save webstrand/b0f79ef6ed37839d1432466fe8ddbc1a to your computer and use it in GitHub Desktop.
Save webstrand/b0f79ef6ed37839d1432466fe8ddbc1a to your computer and use it in GitHub Desktop.
Functions and type aliases for unit testing compile-time types.
/**
* 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