Recently Typescript the project to document a problem encountered during migration.

The problem background

The following code defines a User interface, Company interface, Order interface, and corresponding Mongoose Model. User has a foreign key associated with Company and many foreign key associated with Orders.

interface IUser & Document{
  _id: string;
  company: string | ICompany;
  orders: string[] | IOrder[];
}

interface ICompany & Document{
  _id: string;
  name: string;
}

interface IOrder & Document{
  _id: string
  title: string;
}

const userSchema: Schema = new Schema({
  company: { type: ObjectId, ref: 'Company' },
  orders: [{ type: ObjectId, ref: 'Order'}}]);const companySchema: Schema = new Schema({
  name: String
});

const orderSchema: Schema = new Schema({
  title: String
});

export const User: Model<IUser> = mongoose.model('User', userSchema);
const Company: Model<ICompany> = mongoose.model('Company', companySchema);
const Order: Model<IOrder> = mongoose.model('User', orderSchema);
Copy the code

Problem reproduction

The following scenarios may occur:

import { User } from 'models'
User.findOne()
.populate({ 'path': 'company' })
.populate({ 'path': 'orders'})
.then(user= > {
    // The compiler error occurs when you attempt to access the properties of the company object generated by populate
    // Property '_id' does not exist on type 'string | ICompany'.
    // Property '_id' does not exist on type 'string'.
    const companyId = user.company._id

    // If you try to access order.map here, the compiler will report an error
    // Cannot invoke an expression whose type lacks a call signature. 
    // '((callbackfn: (value: string, index: number, array: string[]) => U, thisArg? : any) => U[])
    // | ((callbackfn: (value: TOrder, index: number, array: TOrder[]) => U, thisArg? : any) => U[])'
    // has no compatible call signatures.ts(2349)
    user.orders.map(order= > order)
})

Copy the code

Both of these questions address the issue of understanding union types. When I wrote these two lines of code, I took it for granted that the union type meant or. That is, depending on whether the populate method is called:

  • Company can be either a string or a company object
  • Orders can be an array of strings or an array of Order objects

User.phany. _id and user.orders. Map should be called without problems.

Question why

So why does the compiler report an error? A close reading of the document reveals:

If a value is a Union type, we can access only the members common to all Types of that Union type.

The union type here is not meant as a union, but as an intersection.

  • stringThere is nostring._id. The compiler reported an error.

The second problem, however, is more complicated and has been suggested as a solution, so I’ll talk about that later.

Problem solving

So how do we solve this situation?

Type assertion/Overload Type Assertion

For the first question, I had a Type Assertion

const company = user.company
const companyId = (company as ICompany)._id
Copy the code

The compiler doesn’t get an error, which fixes the problem, but it’s not a very safe operation. You need to be clear when you write code about when you populate and when you don’t populate. (Although it seems simple, this actually increases the chance of a human error)

So I read the document again and found:

Type Guard takes advantage of typescript’s custom type guard so that the compiler can validate the type without reporting errors.

function isCompany(obj: string | ICompany) :obj is Company {
    return obj && obj._id;
}

import { User } from 'models'
User.findOne()
.populate({ 'path': 'company' })
.populate({ 'path': 'orders'})
.then(user= > {
    if(isCompany(user.company)
      const companyId = user.company._id
    // ...
})
Copy the code

Array type definitions use mixed types

Both types are arrays. Why can’t we call map?

Let’s start with a solution:

interface IUser & Document{
  / / the original statement the orders: string [] | IOrder [];
  orders: (string | IOrder)[]
}
Copy the code

This solves the compiler problem, but in fact this declaration allows mixed arrays like [‘id’, order] to exist. We need either [‘id’,’id’] or [order, order].

This issue has also been mentioned many times on Github, dating back to 2016, and is still being mentioned recently. Call signatures of Union types

In addition, 11 days ago, someone opened a new issue Wishlist: Support for Correlated Record Types, hoping to solve this problem.

Typescript developer Ryan also commented on developer Jcalz in a recent issue.

Will continue to pay attention to this issue.