I have a field that could be an id or populated and I am trying to type this field with TypeScript
import { InferSchemaType, Schema, Types, model } from 'mongoose';
const personSchema = new Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
});
const storySchema = new Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
});
export type PersonDao = { _id: Types.ObjectId } & InferSchemaType<typeof personSchema>;
export type StoryDao = { _id: Types.ObjectId; author: Types.ObjectId | PersonDao } & Omit<InferSchemaType<typeof storySchema>, 'author'>;
export const Person = model('Person', personSchema);
export const Story = model('Story', storySchema);
Story.findOne({ _id: 'fakeId' })
.populate<StoryDao>('author')
.exec()
.then((story: StoryDao) => {
console.log(story.author.name); // error
});
In the console.log name attribute I have this error
Property 'name' does not exist on type 'ObjectId | PersonDao'.
Property 'name' does not exist on type 'ObjectId'.ts(2339)
If I define the type like this
export type StoryDao = { _id: Types.ObjectId; author: PersonDao } & Omit<InferSchemaType<typeof storySchema>, 'author'>;
export const story: StoryDao = {
_id: new Types.ObjectId(),
author: new Types.ObjectId(), // error
title: 'fakeTitle',
};
I don't have the error on name anymore but I have it on author when I create a StoryDao object
type 'ObjectId' is not assignable to type 'PersonDao'.
Type 'ObjectId' is missing the following properties from type '{ createdAt: NativeDate; updatedAt: NativeDate; }': createdAt, updatedAtts(2322)
It's seem's it is a ts limitation on unions because I have the same issue here
type Test = number | { name: string }
const test: Test = {} as Test;
console.log('DEBUG: ', test.name); // error
How do you type this kind of field ?
TypeScript is not able to infer the type of the subdocument at runtime, so since author
is defined as either ObjectId | PersonDao
, it's assuming that it may be an ObjectId
even after being populated, which does not have a name
property. It's technically not wrong here, in the event that the population fails to produce a complete Author record. Union types (TypeA | TypeB
) can be especially difficult to make TS cooperate with, since it's not aware of the logic that generally determines when a type should be one thing, as opposed to another.
Either you can just use a type assertion to tell TS that this story is populated, e.g.:
const author = story.author as PersonDao;
console.log(author.name);
Or if you want to avoid using a kludge like so you can add a typeguard function to check before accessing any questionable properties, a rough example would look like this:
const isPopulatedAuthor = (x: any): x is PersonDao => {
return x.name !== undefined
}
if (isPopulatedAuthor(story.author)) {
console.log(story.author.name)
}