Reputation: 7609
I have an interface
interface Test {
property: SpecialProperty;
}
now SpecialProperty
can be ONE of each :
export type SpecialProperty= {
withPropA: PropA;
}
| {
withPropB: PropB;
}
| {
withPropC: PropC;
}
there can't be withPropA
and another property etc..
The problem is the compiler do not allow me to do this.
if I do
function(param: Test ) {
if(param.property.withPropA) {
const prop:PropA = param.property.withPropA;
}
if(param.property.withPropB) {
const prop:PropB = param.property.withPropB;
}
}
the compiler say
Property 'withPropA' does not exist on type '{ withPropB: PropB; }'.
I tried casting etc... but nothing
Upvotes: 0
Views: 61
Reputation: 33041
To go with union types, you should declare some common property for all unions.
For example type
or kind
.
See next example:
interface Test {
property: SpecialProperty;
}
type PropA = 'a'
type PropB = 'b'
type PropC = 'c'
//https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
export type SpecialProperty =
| {
type: 'A',
withPropA: PropA;
}
| {
type: 'B',
withPropB: PropB;
}
| {
type: 'C'
withPropC: PropC;
}
function test(param: Test) {
if (param.property.type === 'A') {
const prop: PropA = param.property.withPropA;
}
if (param.property.type === 'B') {
const prop: PropB = param.property.withPropB;
}
}
Because this is how TypeScript discrimunated unions works
I know, I know, now you might say to me:
Wait, I can't change my SpecialProperty
, because this type is from third party library, I don't have control over this type.
In this case you can use next utils:
type PropA = 'a'
type PropB = 'b'
type PropC = 'c'
export type SpecialProperty =
| {
withPropA: PropA;
}
| {
withPropB: PropB;
}
| {
withPropC: PropC;
}
// credits goes to https://stackoverflow.com/questions/65805600/struggling-with-building-a-type-in-ts#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type StrictUnionWrapper = StrictUnion<SpecialProperty>
interface Test {
property: StrictUnionWrapper;
}
function test(param: Test) {
if (param.property.withPropA) {
const prop: PropA = param.property.withPropA;
}
if (param.property.withPropB) {
const prop: PropB = param.property.withPropB;
}
}
Personally, I use next typeguard for such kind of check:
const hasProperty = <T, U extends string>(obj: T, prop: U): obj is T & Record<U, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const foo = (arg: unknown) => {
if (hasProperty(arg, 'age')) {
const b = arg // Record<"age", unknown>
b.age // ok
}
}
Upvotes: 0
Reputation: 1074188
You need to use a property existence check to narrow the type of param.property
rather than a truthiness check:
function example(param: Test ) {
if ("withPropA" in param.property) { // ***
const prop:PropA = param.property.withPropA;
}
if ("withPropB" in param.property) { // ***
const prop:PropB = param.property.withPropB;
}
}
Your truthiness check required that you read the value of the property, but TypeScript doesn't know whether the object has that property when you ask it to do that.
More here. That page is supposedly deprecated in favor of this page now, but the new page doesn't cover this particular check (yet, the new page is brand new).
Upvotes: 3