Skip to content

RequiredProps<T>: Unexpected result when T = {}  #21988

Closed
@yortus

Description

@yortus

TypeScript Version: 2.8.0-dev.20180216

Code

// Type-level filters to extract just the required or optional properties of a type
// Defns from https://github.com/Microsoft/TypeScript/pull/21919#issuecomment-365491689
type RequiredPropNames<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
type OptionalPropNames<T> = { [P in keyof T]: undefined extends T[P] ? P : never }[keyof T];
type RequiredProps<T> = { [P in RequiredPropNames<T>]: T[P] };
type OptionalProps<T> = { [P in OptionalPropNames<T>]: T[P] };

// Some object types with different numbers of props
type P2 = {a: string, b: number};       // Two props
type P1 = {a: string};                  // One prop
type P0 = {};                           // No props

// Let's extract only the required properties of P0, P1, and P2
type P2Names = RequiredPropNames<P2>;   // P2Names = "a" | "b"                  ✓😊
type P1Names = RequiredPropNames<P1>;   // P1Names = "a"                        ✓😊
type P0Names = RequiredPropNames<P0>;   // P0Names = any                        ?😕

type P2Props = RequiredProps<P2>;       // P2Props = { a: string; b: number; }  ✓😊
type P1Props = RequiredProps<P1>;       // P1Props = { a: string; }             ✓😊
type P0Props = RequiredProps<P0>;       // P0Props = { [x: string]: any }       ?😕

Expected behavior:
P0Names = never and P0Props = {}

Actual behavior:
P0Names = any and P0Props = {[x: string]: any}

Notes:
@ahejlsberg helpfully came up with the RequiredProps and OptionalProps definitions above from a question I asked in #21919. They work great except when T = {}

Not 100% sure if this is a bug or not, but it's definitely counterintuitive. RequiredProps works for any object type except the empty one, when a string indexer suddenly appears. It certainly breaks the semantic expectations of RequiredProps.

Workaround:
The following version of RequiredPropNames achieves the expected behaviour by singling out the {} case:

type RequiredPropNames<T> =
    keyof T extends never
        ? never
        : {[P in keyof T]: undefined extends T[P] ? never : P}[keyof T];

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions