microsoft / microsoft/TypeScript

Using `undefined` as a discriminator within a mapped type yields erroneous types when primitive intersections are involved

Open
#14,471 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Needs Investigation
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

TypeScript Version: 2.2.0

Code

// A *self-contained* demonstration of the problem follows...
type DeepReadonly<T> = {
    readonly [K in keyof T]: DeepReadonly<T[K]>;
}

interface FieldBrand {
    " do not use ": void;
}

type FieldId = number & FieldBrand;

interface DefOne {
    field: string | FieldId;
    kind: string;
}

interface DefTwo {
    field?: undefined; // Allow discriminant checks on 'field'
    value: string;
}

type Def = DefOne | DefTwo;

interface State {
    a?: Def;
    b?: Def;
}

type ROState = DeepReadonly<State>;

function lookupName(f: FieldId): string {
    return "";
}

function remapFieldsToNames(channels: ROState): ROState {
    const newState: State = {};
    for (const k of Object.keys(channels)) {
        const key = k as keyof ROState;
        const ch = channels[key];
        let replacement: ROState[typeof key] | undefined = undefined;
        if (ch) {
            if (ch.field) {
                const f = ch.field;
                if (typeof f === "number") {
                    f; // Should be FieldId or number, not never!
                    replacement = { ...ch, field: lookupName(f) };
                }
                else if (typeof f === "string") {
                    f; // correct
                }
            }
        }
        newState[k] = replacement || channels[k];
    }
    return newState;
}

Expected behavior:
There are no type errors in the above, and f after the typeof f === "number" check is either a number or FieldId.

Actual behavior:
replacement = { ...ch, field: lookupName(f) }; has a type error and f is of type never.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the self-contained TypeScript reproduction from the issue and inspect narrowing for the mapped DeepReadonly type, undefined discriminator, and primitive intersection. The fix is done when the reproduction reports no type errors and the number branch narrows f to number or FieldId rather than never.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.