bastianplsfix

Conditional types

A conditional type is an if at the type level. It has the same shape as JavaScript's ternary operator, and its condition is always the same question: is the type on the left of extends assignable to the type on the right? The other half of the feature is infer, which names a piece of the type you matched against, and it is only legal in the condition of a conditional type, which is why the two belong in one entry. Everything past the first two steps is what happens when the argument is a union, which is most of the time.

Create programs/conditional-types.test.ts for this reference and keep it open, starting with the imports:

import { assertEquals, assertStrictEquals } from "@std/assert";
import { assertType, type IsExact } from "@std/testing/types";

Below the imports, add the types, functions, and classes the whole page shares; each earns its explanation in its step.

type Defined<T> = T extends null | undefined ? never : T;

type Unwrapped<T> = T extends Promise<infer Value> ? Value : T;

type IsString<T> = T extends string ? true : false;

type IsStringExactly<T> = [T] extends [string] ? true : false;

function repeat(text: string, times: number): string {
return text.repeat(times);
}

class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

abstract class Shape {
abstract area(): number;
}

Follow the page as you add and revise the runnable examples below it.

an if at the type level

Deno.test("an if at the type level", () => {
const kept: Defined<string | null> = "oak";
// @ts-expect-error: Type 'null' is not assignable to type 'string'.
const dropped: Defined<string | null> = null;

assertType<IsExact<Defined<string | null>, string>>(true);
assertType<IsExact<Defined<string | null>, NonNullable<string | null>>>(
true,
);

const fromPromise: Unwrapped<Promise<string>> = "oak";
const fromPlain: Unwrapped<number> = 123;

assertEquals([kept, dropped, fromPromise, fromPlain], [
"oak",
null,
"oak",
123,
]);
});
Check programs/conditional-types.test.ts
running 1 test from ./programs/conditional-types.test.ts
an if at the type level ... ok (278µs)

ok | 1 passed | 0 failed (1ms)

Defined<string | null> is string: the conditional asked its question, the never branch dropped the null, and the depths come back to how. TypeScript ships this exact type as NonNullable<T>, which the second IsExact proves is the same type rather than a similar one. Unwrapped is the other half of the feature in one line: T extends Promise<infer Value> matches the shape and names what was inside, so Unwrapped<Promise<string>> is string and a plain number falls through untouched.

a generic type is a function that takes types

Deno.test("a generic type is a function that takes types", () => {
type Pair<T> = [T, T];
type NumericPair<T extends number = 0> = [T, T];

const pair: Pair<string> = ["a", "b"];
const legal: NumericPair<123> = [123, 123];
const defaulted: NumericPair = [0, 0];

// @ts-expect-error: Type 'string' does not satisfy the constraint 'number'.
type IllegalPair = NumericPair<"oak">;

assertEquals(pair, ["a", "b"]);
assertEquals(legal, [123, 123]);
assertEquals(defaulted, [0, 0]);
});
a generic type is a function that takes types ... ok (72µs)

A conditional type is only useful inside a generic type, so this step is the ground floor: a generic type is a function that takes types and returns a type. extends number constrains what may be passed, the way a parameter's type annotation does at the program level, and = 0 is a default argument. The refusal is a compile error at the point of the call, TS2344, not something that fails later. Type parameters on functions, and how they get inferred from arguments at a call site, are a different subject this entry stays away from: everything here is called explicitly, with its type arguments written out.

assignability, not identity

Deno.test("assignability, not identity", () => {
type IsNumber<T> = T extends number ? true : false;

const literal: IsNumber<123> = true;
const wide: IsNumber<number> = true;
const other: IsNumber<"oak"> = false;

// @ts-expect-error: Type 'false' is not assignable to type 'true'.
const wrong: IsNumber<123> = false;

assertEquals([literal, wide, other, wrong], [true, true, false, false]);
});
assignability, not identity ... ok (33µs)

IsNumber<123> is true and so is IsNumber<number>. The condition is assignability, not identity: 123 is assignable to number, so it passes, and a conditional type cannot ask "is this type exactly number", which is the question the testing types page's IsExact exists to answer. Notice what these assertions are: true and false are types with one value each, so a predicate at the type level returns something you can annotate a real variable with, the annotation is the claim, and the checker is the test.

a chain of conditions, checked in order

The chain below imitates the typeof operator. Predict that they agree on null:

Deno.test("a chain of conditions, checked in order", () => {
type PrimitiveName<T> = T extends undefined ? "undefined"
: T extends null ? "null"
: T extends boolean ? "boolean"
: T extends number ? "number"
: T extends bigint ? "bigint"
: T extends string ? "string"
: "object";

const forBigint: PrimitiveName<123n> = "bigint";
const forString: PrimitiveName<"oak"> = "string";
const forDate: PrimitiveName<Date> = "object";
const forNull: PrimitiveName<null> = "null";

assertStrictEquals(typeof 123n, forBigint);
assertStrictEquals(typeof "oak", forString);
assertStrictEquals(typeof new Date(0), forDate);
assertStrictEquals(typeof null, forNull);
});
Check programs/conditional-types.test.ts
running 4 tests from ./programs/conditional-types.test.ts
...
a chain of conditions, checked in order ... FAILED (8ms)

ERRORS

a chain of conditions, checked in order => ./programs/conditional-types.test.ts:79:6
error: AssertionError: Values are not strictly equal.

[Diff] Actual / Expected

- object
+ null

FAILURES

a chain of conditions, checked in order => ./programs/conditional-types.test.ts:79:6

FAILED | 3 passed | 1 failed (9ms)

error: Test failed

They disagree, and the type-level version is the one that is right. typeof null is "object", the oldest bug in the language, which the nothing, twice page tells in full, and PrimitiveName<null> answers "null" because its conditions are checked in order and the null arm comes second. Correct the prediction and pin both answers:

Deno.test("a chain of conditions, checked in order", () => {
type PrimitiveName<T> = T extends undefined ? "undefined"
: T extends null ? "null"
: T extends boolean ? "boolean"
: T extends number ? "number"
: T extends bigint ? "bigint"
: T extends string ? "string"
: "object";

const forBigint: PrimitiveName<123n> = "bigint";
const forString: PrimitiveName<"oak"> = "string";
const forDate: PrimitiveName<Date> = "object";
const forNull: PrimitiveName<null> = "null";

assertStrictEquals(typeof 123n, forBigint);
assertStrictEquals(typeof "oak", forString);
assertStrictEquals(typeof new Date(0), forDate);
assertStrictEquals(typeof null, "object");
assertStrictEquals(forNull, "null");
});
a chain of conditions, checked in order ... ok (558µs)

Chaining works exactly as the ternary operator does, and it reads the same way if you let the formatter lay it out: deno fmt puts each : at the start of a line and lines them up, which is the layout above. One consequence is worth knowing in advance: a conditional nested in the true branch comes out much less readable than one nested in the false branch, and when that happens, pull the nested conditional out into its own named type.

it is applied to each member of a union

Deno.test("it is applied to each member of a union", () => {
type Wrapped<T> = T extends string ? Array<T> : T;

const wrapped: Wrapped<"a" | 1> = ["a"];
const notWrapped: Wrapped<"a" | 1> = 1;

assertType<IsExact<Wrapped<"a" | 1>, Array<"a"> | 1>>(true);

assertEquals(wrapped, ["a"]);
assertStrictEquals(notWrapped, 1);
});
it is applied to each member of a union ... ok (44µs)

Wrapped<"a" | 1> is not one answer. The conditional is applied to "a" and to 1 separately and the results are unioned back together, giving Array<"a"> | 1. This is called distributivity, and it is the single most important thing to know about conditional types: it is how you loop over a union, and it is how a predicate you wrote gives you an answer you did not ask for. The useful case first: distribution is why Defined in the first step worked at all, because it was applied to string and to null in turn, and the null arm produced never, which disappears from a union.

a predicate that returns boolean has told you nothing

Deno.test("a predicate that returns boolean has told you nothing", () => {
const yes: IsString<string | number> = true;
const no: IsString<string | number> = false;

assertType<IsExact<IsString<string | number>, boolean>>(true);
assertType<IsExact<IsStringExactly<string | number>, false>>(true);
assertType<IsExact<IsStringExactly<string>, true>>(true);

assertStrictEquals(yes, true);
assertStrictEquals(no, false);
});
a predicate that returns boolean has told you nothing ... ok (21µs)

This is the case that costs people an afternoon, and the testing types page met it while building an equality helper. Both true and false are legal values for IsString<string | number>, which is what "it gave two answers" means when you look at it from the value side: the type is true | false, which is boolean. [T] extends [string] is the fix. A conditional distributes only when the left of extends is a bare type parameter, so wrapping both sides in a single-element tuple turns distribution off while asking the same question, and IsStringExactly<string | number> is false, the answer you wanted. The rule to carry: if the question is about the union as a whole, bracket it; if the question is about each member, do not.

only the left-hand side distributes

Deno.test("only the left-hand side distributes", () => {
// deno-lint-ignore no-explicit-any
type Distributed<T> = T extends any ? Array<T> : never;
// deno-lint-ignore no-explicit-any
type NotDistributed<T> = any extends T ? Array<T> : never;

assertType<IsExact<Distributed<"a" | "b">, Array<"a"> | Array<"b">>>(true);
assertType<IsExact<NotDistributed<"a" | "b">, Array<"a" | "b">>>(true);

// `boolean` is a union of two literal types, which this makes visible.
assertType<IsExact<Distributed<boolean>, Array<false> | Array<true>>>(true);

const separate: Distributed<"a" | "b"> = ["a"];
// @ts-expect-error: Type '("a" | "b")[]' is not assignable to type '"a"[] | "b"[]'.
const mixed: Distributed<"a" | "b"> = ["a", "b"];
const together: NotDistributed<"a" | "b"> = ["a", "b"];

assertEquals(separate, ["a"]);
assertEquals(mixed as unknown, together);
});
only the left-hand side distributes ... ok (25µs)

Two conditionals whose conditions are both always true, and they return different things. T extends any distributes, so you get an array type per member; any extends T does not, so you get one array of the union. Nothing is being tested in either case, because the extends is there purely to trigger the loop, which is a technique rather than a check and is worth a comment when you use it. Distributed<boolean> giving Array<false> | Array<true> is the same mechanism telling you something about boolean: it is a union of two literal types, and anything that distributes will treat it as two cases. The shielded line is the probe technique doing its job, because the refusal prints the distributed type. Strip the shield in a copy of the file and read the answer:

Check programs/conditional-types.test.ts
TS2322 [ERROR]: Type '("a" | "b")[]' is not assignable to type '"a"[] | "b"[]'.
Type '("a" | "b")[]' is not assignable to type '"a"[]'.
Type '"a" | "b"' is not assignable to type '"a"'.
Type '"b"' is not assignable to type '"a"'.
const mixed: Distributed<"a" | "b"> = ["a", "b"];
~~~~~
at file:///programs/conditional-types.test.ts:137:9

error: Type checking failed.

An array holding both members is fine for NotDistributed and refused for Distributed, because Array<"a"> | Array<"b"> means an array of one or an array of the other, never a mixture. If you ever doubt whether a type distributed, assign the wrong value to it and read the message: '"a"[] | "b"[]' is the answer, printed, with the nested lines walking the refusal down member by member.

never breaks a predicate in two ways

Deno.test("never breaks a predicate in two ways", () => {
assertType<IsExact<IsString<never>, never>>(true);
assertType<IsExact<IsStringExactly<never>, true>>(true);

// @ts-expect-error: Type 'true' is not assignable to type 'never'.
const impossible: IsString<never> = true;
const decided: IsStringExactly<never> = true;

assertType<IsExact<never extends true ? "yes" : "no", "yes">>(true);
assertType<IsExact<never extends false ? "yes" : "no", "yes">>(true);
assertType<IsExact<never extends never ? "yes" : "no", "yes">>(true);

assertStrictEquals(impossible, decided);
});
never breaks a predicate in two ways ... ok (19µs)

IsString<never> is neither true nor false. It is never, and no value can be assigned to it at all, which is what the shield pins. The reason follows from the previous two steps rather than being a special case: never is the empty union, so distributing over it means running the conditional zero times and unioning zero results, which is never, while the bracketed version does not distribute and decides normally. The second way is the opposite problem: never is assignable to every type, so a condition with a concrete never on the left always takes the true branch. Both facts come out of the any, unknown, and never page: the empty set is a subset of everything, and it has no members to loop over. Together they mean a hand-written predicate is usually wrong for never in one of two directions, and this is not a corner case you can ignore, because never is what a filtered union collapses to, which is the next step.

never as a branch is how you filter a union

Deno.test("never as a branch is how you filter a union", () => {
type Status = "queued" | "running" | "done" | 1 | 2;

type DropNumbers<T> = T extends number ? never : T;
type KeepNumbers<T> = T extends number ? T : never;

assertType<IsExact<DropNumbers<Status>, "queued" | "running" | "done">>(
true,
);
assertType<IsExact<KeepNumbers<Status>, 1 | 2>>(true);

type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;

assertType<IsExact<MyExclude<Status, number>, Exclude<Status, number>>>(
true,
);
assertType<IsExact<MyExtract<Status, number>, Extract<Status, number>>>(
true,
);

// The same two filters over the same members, at run time.
const members: Array<Status> = ["queued", "running", "done", 1, 2];
assertEquals(members.filter((m) => typeof m !== "number"), [
"queued",
"running",
"done",
]);
assertEquals(members.filter((m) => typeof m === "number"), [1, 2]);
});
never as a branch is how you filter a union ... ok (44µs)

Distribution loops over the members, and returning never for a member removes it, because never in a union contributes nothing: two lines and you have filtering. Make the test a parameter and you have the two built-ins, definition for definition, because MyExclude and MyExtract are the whole of Exclude<T, U> and Extract<T, U> as the standard library defines them, and the IsExact assertions say the reproductions are the same types rather than similar ones. Reach for the built-in names, since a reader recognises them and Exclude reads as the set operation it is. The run-time half is the comparison worth making: same operation at both levels, a predicate per member, and the ones that fail are gone. filter returns a shorter array and Exclude returns a smaller union, and knowing they are the same idea is most of what makes type-level code readable.

infer is destructuring for types

Deno.test("infer is destructuring for types", () => {
type ElementType<T> = T extends Array<infer Element> ? Element : never;

function firstOf<T extends Array<unknown>>(list: T): ElementType<T> {
const [first] = list;
return first as ElementType<T>;
}

const first: string = firstOf(["a", "b"]);

assertType<IsExact<ElementType<Array<string>>, string>>(true);
assertType<IsExact<ElementType<string>, never>>(true);

const args: Parameters<typeof repeat> = ["ab", 2];
const returned: ReturnType<typeof repeat> = repeat(...args);
const made: ConstructorParameters<typeof Point> = [1, 2];
const point: InstanceType<typeof Point> = new Point(...made);

assertType<IsExact<Awaited<Promise<Promise<number>>>, number>>(true);

assertStrictEquals(first, "a");
assertStrictEquals(returned, "abab");
assertStrictEquals(point.x, 1);
});
infer is destructuring for types ... ok (42µs)

T extends Array<infer Element> reads as a pattern: if T has the shape Array<something>, call that something Element. It is the same move as const [first] = list one line below it, at the other level, and the comparison is the fastest way to understand infer. Four built-in types are one infer each: Parameters<F> matches F against (...args: infer P) => any and returns P, ReturnType<F> infers the other side, and ConstructorParameters and InstanceType do the same against a construct signature, the pair the classes as values page introduced without deriving. Awaited<T> is the recursive version of the module-scope Unwrapped, which is why the nested promise comes out flat.

an abstract class has no construct signature

One detail from the construct-signature pair is worth its own step, because it is the reason Class<T> is usually written with abstract in it. Strip the shield and the refusal is specific:

Deno.test("an abstract class has no construct signature", () => {
// deno-lint-ignore no-explicit-any
type Concrete<T> = new (...args: Array<any>) => T;
// deno-lint-ignore no-explicit-any
type AnyClass<T> = abstract new (...args: Array<any>) => T;

const shapeClass: Concrete<Shape> = Shape;
const anyClass: AnyClass<Shape> = Shape;

assertStrictEquals(shapeClass, anyClass);
});
Check programs/conditional-types.test.ts
TS2322 [ERROR]: Type 'typeof Shape' is not assignable to type 'Concrete<Shape>'.
Cannot assign an abstract constructor type to a non-abstract constructor type.
const shapeClass: Concrete<Shape> = Shape;
~~~~~~~~~~
at file:///programs/conditional-types.test.ts:222:9

error: Type checking failed.

abstract new (...) includes both kinds of class and new (...) includes only the ones you can instantiate, which is why the AnyClass line compiles while the Concrete line does not: Shape has no construct signature to offer. If you want to accept any class, write the abstract; if you want to construct one, leave it out and let this error do its job, the same split the classes as values page builds its Class<T> on. Shield the refusal:

Deno.test("an abstract class has no construct signature", () => {
// deno-lint-ignore no-explicit-any
type Concrete<T> = new (...args: Array<any>) => T;
// deno-lint-ignore no-explicit-any
type AnyClass<T> = abstract new (...args: Array<any>) => T;

// @ts-expect-error: Cannot assign an abstract constructor type to a non-abstract constructor type.
const shapeClass: Concrete<Shape> = Shape;
const anyClass: AnyClass<Shape> = Shape;

assertStrictEquals(shapeClass, anyClass);
});
an abstract class has no construct signature ... ok (11µs)

when inference needs help

infer First gives you unknown, because nothing in the pattern said what kind of thing the first element is. Strip the shield from the recursive type and the compiler reports it twice, then goes further than usual:

Deno.test("when inference needs help", () => {
type EnumFromTuple<T extends ReadonlyArray<string>> = T extends [
infer First,
...infer Rest,
] ? Record<First, First> & EnumFromTuple<Rest>
: Record<never, never>;

type Constrained<T extends ReadonlyArray<string>> = T extends [
infer First extends string,
...infer Rest extends ReadonlyArray<string>,
] ? Record<First, First> & Constrained<Rest>
: Record<never, never>;

const tree: Constrained<["Maple", "Oak"]> = { Maple: "Maple", Oak: "Oak" };

// @ts-expect-error: Type '"Maple"' is not assignable to type '"Oak"'.
const wrong: Constrained<["Maple", "Oak"]> = { Maple: "Maple", Oak: "Maple" };

assertEquals(tree, { Maple: "Maple", Oak: "Oak" });
assertEquals(wrong.Oak, "Maple");
});
Check programs/conditional-types.test.ts
TS2344 [ERROR]: Type 'First' does not satisfy the constraint 'string | number | symbol'.
] ? Record<First, First> & EnumFromTuple<Rest>
~~~~~
at file:///programs/conditional-types.test.ts:233:14

TS2208 [ERROR]: This type parameter might need an `extends string | number | symbol` constraint.
infer First,
~~~~~
at file:///programs/conditional-types.test.ts:231:11

TS2344 [ERROR]: Type 'Rest' does not satisfy the constraint 'readonly string[]'.
Type 'unknown[]' is not assignable to type 'readonly string[]'.
Type 'unknown' is not assignable to type 'string'.
] ? Record<First, First> & EnumFromTuple<Rest>
~~~~
at file:///programs/conditional-types.test.ts:233:44

Found 2 errors.

error: Type checking failed.

Record needs a key, so First is refused, EnumFromTuple<Rest> needs an array, so Rest is refused, and the TS2208 in the middle is the compiler pointing at the infer itself and naming the fix. Do what it says, which is what the Constrained version in the same test already does: infer X extends C constrains what may be inferred, and both constraints are needed here, a string to be a key and an array to be passed back in. Shield the broken original to keep both on the page:

Deno.test("when inference needs help", () => {
type EnumFromTuple<T extends ReadonlyArray<string>> = T extends [
infer First,
...infer Rest,
// @ts-expect-error: First is not a legal key, Rest is not an array
] ? Record<First, First> & EnumFromTuple<Rest>
: Record<never, never>;

type Constrained<T extends ReadonlyArray<string>> = T extends [
infer First extends string,
...infer Rest extends ReadonlyArray<string>,
] ? Record<First, First> & Constrained<Rest>
: Record<never, never>;

const tree: Constrained<["Maple", "Oak"]> = { Maple: "Maple", Oak: "Oak" };

// @ts-expect-error: Type '"Maple"' is not assignable to type '"Oak"'.
const wrong: Constrained<["Maple", "Oak"]> = { Maple: "Maple", Oak: "Maple" };

assertEquals(tree, { Maple: "Maple", Oak: "Oak" });
assertEquals(wrong.Oak, "Maple");
});
when inference needs help ... ok (20µs)

Two diagnostics, two constraints, and the entry that wanted this type in the first place is enum patterns, whose createEnum reaches the same result with as const and no recursion at all.

a deferred conditional type refuses every concrete value

Deno.test("a deferred conditional type refuses every concrete value", () => {
type Kind = "string" | "number";
type ValueFor<K extends Kind> = K extends "string" ? string : number;

function refused<K extends Kind>(kind: K): ValueFor<K> {
// @ts-expect-error: Type 'string' is not assignable to type 'ValueFor<K>'.
if (kind === "string") return "oak";
// @ts-expect-error: Type 'number' is not assignable to type 'ValueFor<K>'.
return 123;
}

function asserted<K extends Kind>(kind: K): ValueFor<K> {
return (kind === "string" ? "oak" : 123) as unknown as ValueFor<K>;
}

function notEnough<K extends Kind>(kind: K): ValueFor<K> {
// @ts-expect-error: Type 'unknown' is not assignable to type 'ValueFor<K>'.
return (kind === "string" ? "oak" : 123) as unknown;
}

function fixed(kind: "string"): string;
function fixed(kind: "number"): number;
function fixed(kind: Kind): string | number {
return kind === "string" ? "oak" : 123;
}

const text: string = fixed("string");
const count: number = fixed("number");

assertStrictEquals(refused("string"), "oak");
assertStrictEquals(asserted("number"), 123);
assertStrictEquals(notEnough("string"), "oak");
assertStrictEquals(text, "oak");
assertStrictEquals(count, 123);
});
a deferred conditional type refuses every concrete value ... ok (25µs)

Strip the two shields inside refused and read the errors together:

Check programs/conditional-types.test.ts
TS2322 [ERROR]: Type 'string' is not assignable to type 'ValueFor<K>'.
if (kind === "string") return "oak";
~~~~~~
at file:///programs/conditional-types.test.ts:257:28

TS2322 [ERROR]: Type 'number' is not assignable to type 'ValueFor<K>'.
return 123;
~~~~~~
at file:///programs/conditional-types.test.ts:258:5

Found 2 errors.

error: Type checking failed.

Inside the body, K is still unknown, so ValueFor<K> has not been decided: it is a deferred conditional type, and it is neither string nor number nor their union, the same undecided shape the testing types page caught IsExact refusing to judge. Narrowing kind does not help, because narrowing a value tells the checker nothing about the type parameter, so both returns are refused and every value you could write there would be. Three casts get you past it and one that looks like it should does not: as unknown as ValueFor<K> works, as any works, and as unknown on its own does not, because unknown is not assignable to anything. Whichever you pick, the body is now trusted rather than checked, which is the real cost: you have written the function's contract twice, once as a type and once as code, and only one of them is verified. Which is why fixed deserves the look: two overload signatures give callers the same experience, and the implementation signature is a plain union the body genuinely satisfies. The overloading page is the entry for that trade, and it is usually the better one for two or three cases, because a conditional return type earns its keep when the mapping is a rule rather than a list.

infer also declares a local name

Deno.test("infer also declares a local name", async () => {
type WrapTriple<T> = Promise<T> extends infer W ? [W, W, W] : never;
type WrapTripleWithDefault<T, W = Promise<T>> = [W, W, W];

assertType<IsExact<WrapTriple<number>, WrapTripleWithDefault<number>>>(true);

const triple: WrapTriple<number> = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
];

assertEquals(await Promise.all(triple), [1, 2, 3]);
});
infer also declares a local name ... ok (21µs)

The type level has no const, so infer gets pressed into service as one: X extends infer W ? ... : never computes X once and calls it W, and the condition is always true and exists only to introduce the name. The second form does the same job with a defaulted type parameter and no conditional, and it is easier to read. Reach for the infer version when the type you want to name is not expressible as a parameter default, which is rarer than the trick's popularity suggests.

The whole entry

Run the whole reference suite:

Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/branding.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes-as-values.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conditional-types.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/enum-patterns.test.ts
Check programs/enums.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/keyof-and-indexed-access.test.ts
Check programs/loops.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/testing-types.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/typing-arrays.test.ts
Check programs/typing-classes.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 9 tests from ./programs/branding.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 10 tests from ./programs/classes-as-values.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 14 tests from ./programs/conditional-types.test.ts
an if at the type level ... ok (280µs)
a generic type is a function that takes types ... ok (52µs)
assignability, not identity ... ok (23µs)
a chain of conditions, checked in order ... ok (52µs)
it is applied to each member of a union ... ok (36µs)
a predicate that returns boolean has told you nothing ... ok (17µs)
only the left-hand side distributes ... ok (23µs)
never breaks a predicate in two ways ... ok (18µs)
never as a branch is how you filter a union ... ok (37µs)
infer is destructuring for types ... ok (37µs)
an abstract class has no construct signature ... ok (10µs)
when inference needs help ... ok (19µs)
a deferred conditional type refuses every concrete value ... ok (28µs)
infer also declares a local name ... ok (22µs)
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 12 tests from ./programs/enum-patterns.test.ts
...
running 7 tests from ./programs/enums.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/keyof-and-indexed-access.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 14 tests from ./programs/testing-types.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 10 tests from ./programs/typing-arrays.test.ts
...
running 12 tests from ./programs/typing-classes.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...

ok | 772 passed | 0 failed (908ms)

Fourteen tests, and the practice is short. Try the built-in first: Exclude, Extract, NonNullable, Parameters, ReturnType, Awaited, InstanceType, each one conditional type, each already tested, each a name a reader recognises. Decide about distribution every time and say which you meant, because a bare type parameter loops over the union and [T] extends [U] asks about the whole thing; most predicates want the brackets and most transformations want them off. Check never before you trust a predicate, since IsString<never> is never and a never on the left of extends passes every test, so if a filtered union has gone strange, this is the first thing to look at. Read infer as destructuring: if you can write the pattern as a value-level destructuring you can usually write it as an infer, and if you cannot, you are probably about to write something that needs recursion, which is where the tuple types page goes. Prefer two overload signatures to a computed return type when the mapping is a short list, because the conditional version needs a cast, and a cast in a return statement is a place where a bug can live indefinitely. And stop at two levels of nesting: beyond that, name the inner conditional, because the error messages do not get better with depth, and a type test can tell you a type is wrong but not why.