Reputation: 27304
I'm taking a look at enabling the strictNullChecks
flag for a TypeScript project I've been working on, but it creates a lot of compiler errors. Some of them are edge cases I genuinely need to check but a lot of them fall into a particular pattern.
type BusinessObject = {
id: string;
name: string;
requiredValue: number;
// ...
}
class DataStore {
// ...
public addItem(item: BusinessObject): Promise<string>;
public getItem(id: string): Promise<BusinessObject>;
}
function consumeData(store: DataStore){
store.addItem({name:"one", requiredValue:1});
store.getItem("two").then(item => console.log(item.id));
}
The problem is that the id
property is created by the backing database, so it's only "optional" when making a new item. If I make the id
property required (no ?:
), I get a compiler error in the call to addItem
. But if I make the ID property optional, then every reference to it has to include a guard or non-null assertion operator (item.id!
).
The least bad solution I've been able to come up with so far is to cast the argument I pass to addItem
as any
but this disables checking on the other properties of the BusinessObject
, so it's not a great solution. What I'm hoping for is a sort of analog to the non-null assertion operator, that lets me assert that a null assignment is OK, even though the type declaration says that it isn't: something like store.addItem({id: undefined!, name:"one", requiredValue:1});
.
I would also welcome a better / more consistent way of describing this pattern.
Upvotes: 0
Views: 2002
Reputation: 95732
One easy way to avoid the error would be to change the definition of addItem
to:
public addItem(item: Partial<BusinessObject>): Promise<string>
the code will now work, although it makes all of the properties of BusinessObject optional, not just the id
. That might not be a problem if you have sensible defaults for all of the other properties, but otherwise you have to fall back on creating two types with and without the id
.
What you really want of course is something like:
public addItem(item: Omit<BusinessObject, "id">): Promise<string>
There are several proposals for something like this in typescript (e.g. see https://github.com/Microsoft/TypeScript/issues/12215) so there is hope that your code can be simplified in the future.
There's a lot in that issue, so to save you reading all the way through it, here's what seems to work (see also issue 19569):
export type Diff<T extends string, U extends string> = ({[P in T]: P} &
{[P in U]: never} & {[x: string]: never})[T];
export type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;
type BusinessObject = {
id: string;
name: string;
requiredValue: number;
// ...
}
class DataStore {
// ...
public addItem(item: Omit<BusinessObject, "id">): Promise<string> {
return Promise.resolve('foo')};
public getItem(id: string): Promise<BusinessObject> {
return Promise.resolve({} as any as BusinessObject);
};
}
function consumeData(store: DataStore){
store.addItem({name:"one", requiredValue:1});
store.getItem("two").then(item => console.log(item.id));
}
(dummy body code added for addItem
and getItem
to keep the compiler happy, just ignore that.)
Upvotes: 1
Reputation: 27304
I just realized that there is an operator that works more or less as I described:
store.addItem({ id: undefined as any, name: "one", requiredValue: 1 });
I'd still like better long term solutions, like Titian's suggestion, but this is at least an option.
Upvotes: 1
Reputation: 250226
You could have two types, one representing the object when creating, and one representing a saved object
type NewBusinessObject = {
id?: string;
name: string;
requiredValue: number;
// ...
}
type BusinessObject = NewBusinessObject & {
id: string
}
declare class DataStore {
// ...
public addItem(item: NewBusinessObject): Promise<string>;
public getItem(id: string): Promise<BusinessObject>;
}
function consumeData(store: DataStore) {
store.addItem({ name: "one", requiredValue: 1 });
store.getItem("two").then(item => console.log(item.id));
}
Upvotes: 1