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'
};
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.