Reputation: 3790
Let's say I have the following types:
type A = {
role: 'admin',
force?: boolean
}
type B = {
role: 'regular',
}
type Or = A | B;
I want to be able to do:
const or = {
role: 'regular'
} as Or;
const {role, force} = or;
without getting the error:
Property 'force' does not exist on type 'Or'.(2339)
and without using Touching A
and B
I tried to do:
type Merge<Obj> = {
[k in keyof Obj]: Obj[k]
}
const {role, force} = or as Merge<Or>;
but same error
Upvotes: 1
Views: 642
Reputation: 2852
Redneck hack:
const {role, force} = or as any;
But seriously, you can't. Typescript is helping you out here. If you had done this, it would work:
const or: Or =
role: 'admin'
};
const {role, force} = or;
This way the compiler matches it to the type A, which at least has a chance of having force
, so force here is type boolean | undefined
Upvotes: 0
Reputation: 330456
TypeScript object types aren't sealed or exact (as requested in microsoft/TypeScript#12936). Just because a type doesn't mention a property, it doesn't prohibit the property. So technically speaking a value of type {role: "regular"}
might have a property called force
of some completely unknown type:
type A = { role: 'admin', force?: boolean };
type B = { role: 'regular' };
type Or = A | B;
const hmm = { role: 'regular', force: 9000 } as const;
const oops: Or = hmm; // <-- accepted
And by that logic is not safe for you to do what you're trying to do. The only safe destructuring of a value of type Or
into its pieces would make the force
property of type unknown
.
But from here forward, let's assume that issue is not going to arise, and that we assert that the value only has properties known by the compiler, and any properties not mentioned in the type will be of type undefined
.
How then can we "fix" a union type like Or
to be a version that explicitly knows about all keys from the other members so that the compiler lets you index into it with any of these keys?
Here's one possible way:
type FixUnion<T, K extends PropertyKey = T extends unknown ? keyof T : never> =
T extends unknown ? (
T & { [P in Exclude<K, keyof T>]?: never } extends infer O ? {
[P in keyof O]: O[P]
} : never
) : never
First let's make sure it works on Or
:
type FixedOr = FixUnion<Or>;
/* type FixedOr = {
role: 'admin';
force?: boolean | undefined;
} | {
role: 'regular';
force?: undefined;
} */
That looks correct. Both union members have a role
and a force
property. The first member is the same as A
, while the second member is B
with an additional optional property of type undefined
. So now you're allowed to destructure it:
const { role, force } = or as FixUnion<Or>;
// const role: "admin" | "regular"
// const force: boolean | undefined
So how does it work? First let's look at the K
type parameter. I'm actually just using a default value to compute all the keys of every member of the T
union. The type T extends unknown ? keyof T : never
is a distributive conditional type which breaks T
into its union members, grabs their individual keys, and unites them back together into a new union. I sometimes call this operation AllKeys<T>
as shown in Is it possible to get the keys from a union of objects? . For Or
, K
will be "role" | "force"
.
So now K
is all the keys. The body of FixUnion<T>
is another distributive conditional type; for each member of the T
union, we intersect it with { [P in Exclude<K, keyof T>]?: never }
. That's a mapped type with all optional keys (hence the ?
) which are present in K
but not in the current union member (using the Exclude<T, U>
utility type). And the property values are never
, the impossible type. For Or
, the first member will look like A & {}
and the second member will look like B & { force?: never }
. Note that optional properties automatically get undefined
in their domain, so it's the same as B & { force?: undefined }
. The point is that the force
property in the second union member is known to be either missing or undefined
.
Finally, in order to make things prettier, I use a technique to turn intersections like A & {}
into a plain object like {role: "admin", force?: boolean}
, as described in How can I see the full expanded contract of a Typescript type? ; the ... extends infer O ? {[P in keyof O]: O[P]} : never}
does that. So then the first member becomes {role: "admin", force?: boolean}
, and the second becomes {role: "regular", force?: undefined}
.
Let's just test it out on a different union, just to see how it works:
type Foo = FixUnion<{ a: 0 } | { b: 1 } | { a: 2 } | { c: 3 }>;
/* type Foo = {
a: 0;
b?: undefined;
c?: undefined;
} | {
b: 1;
a?: undefined;
c?: undefined;
} | {
a: 2;
b?: undefined;
c?: undefined;
} | {
c: 3;
a?: undefined;
b?: undefined;
} */
All four union members have all three keys, but at least two keys are optional keys of type undefined
.
Upvotes: 3
Reputation: 544
use the Pick
utility to achieve that and here is my solution you might consider helpful:
type A = {
role: 'admin',
force?: boolean
}
type B = {
role: 'regular',
}
type Or = Pick<A & B, 'force' | 'role'>;
const or = {
role: 'regular'
} as Or;
const {role, force} = or;
Upvotes: 0
Reputation: 187272
You cannot access properties of a union type that only some members of that union type have.
You're getting the same error, and for the same reason is this simplified code:
type A = {
role: 'admin',
force?: boolean
}
type B = {
role: 'regular',
}
const obj: A | B =
Math.random() > 0.5 ?
{ role: 'admin', force: true } :
{ role: 'regular' }
obj.force // type error
If you want that property, then you have to prove that it exists first which narrows your union to just the values that that property.
if ('force' in obj) obj.force // fine
You can add the same thing to your playground in order to make typechecking pass.
const mergedOr = or as Merge<Or>;
const force = 'force' in or && or.force; // fine
It's also worth mentioning that this type:
type Merge<Obj> = {
[k in keyof Obj]: Obj[k]
}
Does nothing useful. It maps an object types keys to the value types for those keys, which will exactly the same as the input type. So whatever you think this is supposed to do, it's not doing that.
Upvotes: 2