Reputation: 1576
I have an interface
interface IData {
importantData: string
}
and I want to add isLoading
flag there. If isLoading = false
, then the importantData
is loaded and for sure has to be there. However, if isLoading = true
, then importantData
may be, or may be not there.
interface ILoadedData extends IData {
isLoading: false
}
interface ILoadingData extends Partial<IData> {
isLoading: true
}
So, my final type is a union of these:
type IDataWithLoading = ILoadedData | ILoadingData
If I try it works great with boolean literals
const a:IDataWithLoading = ({ isLoading: false, importantData: 'secret' })
const b:IDataWithLoading = ({ isLoading: false }) // the only error, nice
const c:IDataWithLoading = ({ isLoading: true })
const d:IDataWithLoading = ({ isLoading: true, importantData: 'secret' })
however, at compile time I don't know whether things are loading or not, so:
const random = Math.random() > 0.5
const e:IDataWithLoading = ({ isLoading: random, importantData: 'secret' }) // doesn't work
It complaints that types are incompatible. It kind of makes sense, since I declared cases for true
and false
, not for boolean
. However, I covered every possible case for boolean
, so I sense that TypeScript can understand that and I sense I'm doing something wrong.
If I declare boolean
explicitly, however,
interface ILoadingData extends Partial<IData> {
isLoading: boolean
}
it'll match isLoading = false
with Partial<IData>
, which is not what I want. What should I do?
Upvotes: 8
Views: 5747
Reputation: 328533
UPDATE: 2019-05-30 with the release of TypeScript 3.5 this should be addressed by smarter union type checking. The following applies to 3.4 and below:
This is a known limitation of TypeScript. The compiler makes no attempt to propagate unions down into properties. Usually such a propagation can't happen (e.g., {a: string, b: string} | {a: number, b: number}
cannot be reduced to {a: string | number, b: string | number}
or anything else more useful). Even in the cases like yours where it is possible to do something, it's not cost-effective for the compiler to make the effort. Usually cases like this come down to manually guiding the compiler through possible states.
For example, you could try this:
interface ILoadedDataButNotSureYet extends IData {
isLoading: boolean;
}
interface ILoadedData extends ILoadedDataButNotSureYet {
isLoading: false
}
So an ILoadedDataButNotSureYet
does have data loaded but isLoading
might be true
or false
. Then, you can express IDataWithLoading
as:
type IDataWithLoading = ILoadedDataButNotSureYet | ILoadingData;
This is equivalent to your original definition, but good luck trying to get the compiler to notice that. Anyway, all the specific instances you mentioned still work:
const a: IDataWithLoading = ({ isLoading: false, importantData: 'secret' })
const b: IDataWithLoading = ({ isLoading: false }) // the only error, nice
const c: IDataWithLoading = ({ isLoading: true })
const d: IDataWithLoading = ({ isLoading: true, importantData: 'secret' })
but the last one also works:
const random = Math.random() > 0.5
const e: IDataWithLoading = ({ isLoading: random, importantData: 'secret' }) // works
You might or might not want to actually change IDataWithLoading
. Another way to go is to keep the original definition and just slap the compiler hard enough until it understands that your code is safe. It's not pretty though:
const eLit = { isLoading: random, importantData: 'secret' };
const e: IDataWithLoading = eLit.isLoading ?
{...eLit, isLoading: eLit.isLoading} : {...eLit, isLoading: eLit.isLoading};
// works, but is ugly
Yuck. Here we are using object spread to copy properties, and a separate isLoading
property to take advantage of the type guarding that happens when you inspect eLit.isLoading
. The ternary operator is redundant (both the "then" and "else" clauses are identical) but the compiler gets the message that in each case the value matches IDataWithLoading
. As I said, yuck.
Finally, you could just decide that you're smarter than the compiler and use a type assertion to make it be quiet. This has the advantage of not making you jump through hoops, with the disadvantage that the compiler isn't helping you maintain type safety here:
const e = {
isLoading: random,
importantData: 'secret'
} as IDataWithLoading; // Take that, compiler!
It's up to you how to proceed. Hope that helps; good luck!
Upvotes: 5
Reputation: 21891
The problem is that you are using Typescript for a wrong task. TS is about types, not values, so it cannot know
whether things are loading or not
So your interface should look just like this
interface IData {
isLoading: boolean
importantData: string | null
}
Update
I'm trying to make TS complain if it sees isLoading=false and no importantData and do not complain if it sees isLoading=true regardless of importantData presence. I am 100% sure it's possible.
Replacing this part of your code:
const random = Math.random() > 0.5
const e: IDataWithLoading = ({ isLoading: random, importantData: 'secret' })
To this:
const loaded = Math.random() > 0.5
let e:IDataWithLoading
if(loaded) {
e = { isLoading: false, importantData: 'secret' }
} else {
e = { isLoading: true, importantData: undefined }
}
Should be all you need, if I understand you correctly.
Upvotes: -1