Home > Software design >  Trouble generating subtypes: Resulting Pick<T, never> becomes any instead of never. How to avo
Trouble generating subtypes: Resulting Pick<T, never> becomes any instead of never. How to avo

Time:07-17

Edit:
I believe the real problem is that there isn't / I don't know a type for an empty object?! The following, surprisingly, is not an error:

const empty = {} as const;
const xx: typeof empty = {
  ho: 'hi'
}

while this is

const notEmpty = {
    hasKey: 'andValue'
} as const;

const yy: typeof notEmpty = {
    ho: 'hu' // not assignable
}

There is a github issue regarding this problem, which people seem not to take very seriously. I believe it is!! It's totally inconsistent behaviour!!!



I'm trying to build a complex API object with all kinds of properties and functions from a fairly basic input object.

This requires very challenging TypeScript constructs with mapped and conditional types.

I've come pretty far, but am currently challenged by weirdness: I'm using nested utility types at one point, which can result in an effective Pick<T, never>. Since none of the keys inside T can be assigned to never I'd expect the resulting type to be typeof {}. Instead, it's any.

That is extremely unsafe, and currently blows my approach to doing things.

Is there a way to avoid this? Is this a bug, or something that's unavoidable?

Here a simple reproduction:

type Original = {
    ho: string;
    ha: string;
    hu: string;
}

type Derived = Pick<Original, 'ho'>; 

const thisIsFine: Derived = { // works fine, as intended
    ho: 'yes',
    // ha: 'can\'t be assigned'
}

type ShouldBeEmptyObject = Pick<Original, never>;

const shouldntBePossible: ShouldBeEmptyObject = {
    anythingGoes: "that's not cool"
}


// More practical example
type AnyObject = {
  [key: string]: any
}

// I distinguish paremeter from property by checking if key starts with '$'
type ParameterSegments<T extends AnyObject> = Pick<T, Extract<keyof T, `$${string}`>>;
type PropertySegments<T extends AnyObject> = Omit<T, Extract<keyof T, `$${string}`>>;

const inputObject = { // we'll generates subtypes from this
    property1: 'hoho',
    property2: 'haha',
    $parameter7: 'param'
};

const onlyProperties: PropertySegments<typeof inputObject> = { // works fine
    property1: 'juppie', // must be present
    property2: 'juchei', // must be present
    // property3: "can't be assigned, not in inputObject",
    // $parameter7: "can't be assigned, is parameter"
};

const onlyPropertyInput = { // troublemaker
    propertyX: 'XxX'
};

const onlyParameters: ParameterSegments<typeof onlyPropertyInput> = { // SHIT
    jap: 'no',
    $worries: '!',
    anything: 'goes',
    $nothing: 'isRequired'
};

Playground

CodePudding user response:

I found this Github issue helpful.

The type {} doesn't mean "any empty object", it means "any non-nullish value".

Since the {} type does not prevent us from assigning properties to it, we should use a different type to represent an empty object. The author of the linked comment offers an alternative:

type EmptyObject = Record<string, never>; // or {[k: string]: never}

const a: EmptyObject = { a: 1 };  // expect error
const b: EmptyObject = 1;         // expect error
const c: EmptyObject = () => {};  // expect error
const d: EmptyObject = null;      // expect error
const e: EmptyObject = undefined; // expect error
const f: EmptyObject = {};        // NO ERROR - as expected

This Record type only allows empty objects. We can use this for the types ParameterSegments and PropertySegments.

type ParameterSegments<T extends AnyObject> = 
  {} extends Pick<T, Extract<keyof T, `$${string}`>>
    ? Record<string, never>
    : Pick<T, Extract<keyof T, `$${string}`>>

type PropertySegments<T extends AnyObject> = 
  {} extends Omit<T, Extract<keyof T, `$${string}`>>
    ? Record<string, never>
    : Omit<T, Extract<keyof T, `$${string}`>>

For both types we can check if the result would be an empty object and return Record<string, never> if that is the case.

Playground

  • Related