Skip to content

Commit

Permalink
feat: Add NoDistribute utility
Browse files Browse the repository at this point in the history
Typescript automatically distributes conditional types over unions when the checked type is a naked type param (e.g. `T extends /*...*/`).
Sometimes this is undesirable: so `NoDistribute<T> extends /*...*/` will not distribute if T is a union.
  • Loading branch information
Retsam committed Jun 12, 2019
1 parent 146ab9e commit d5effb8
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ import { ObjectType } from "./objects";
*/
export type NoInfer<T> = T & ObjectType<T>;

/**
* Prevent `T` from being distributed in a conditional type.
* A conditional is only distributed when the checked type is naked type param and T & {} is not a
* naked type param, but has the same contract as T.
*
* @note This must be used directly the condition itself: `NoDistribute<T> extends U`,
* it won't work wrapping a type argument passed to a conditional type.
*
* @see https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types
*/
export type NoDistribute<T> = T & {};

/**
* Mark a type as nullable (`null | undefined`).
* @param T type that will become nullable
Expand Down
35 changes: 35 additions & 0 deletions test/utils/NoDistribute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import test from 'ava';
import { assert } from '../helpers/assert';

import { NoDistribute } from '../../src';

test("can create a conditional type that won't distribute over unions", t => {
type IsString<T> = T extends string ? "Yes" : "No";
type IsStringNoDistribute<T> = NoDistribute<T> extends string ? "Yes" : "No";

/**
* Evaluates as:
* ("foo" extends string ? "Yes" : "No")
* | (42 extends string ? "Yes" : "No")
*/
type T1 = IsString<"foo" | 42>;
assert<T1, "Yes" | "No">(t);
assert<"Yes" | "No", T1>(t);

/**
* Evaluates as:
* ("foo" | 42) extends string ? "Yes" : "No"
*/
type T2 = IsStringNoDistribute<"foo" | 5>;
assert<T2, "No">(t);
assert<"No", T2>(t);
});

test("cannot be used to prevent a distributive conditional from distributing", t => {
type IsString<T> = T extends string ? "Yes" : "No";
// It's the defintion of the conditional type that matters,
// not the type that's passed in, so this still distributes
type Test = IsString<NoDistribute<"foo" | 42>>;
assert<Test, "Yes" | "No">(t);
assert<"Yes" | "No", Test>(t);
});

0 comments on commit d5effb8

Please sign in to comment.