Coderer
Coderer

Reputation: 27304

Force assignment of undefined with strict null checks enabled?

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

Answers (3)

Duncan
Duncan

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

Coderer
Coderer

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

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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

Related Questions