user1779362
user1779362

Reputation: 1010

Typescript Conditional Types Not Figuring Out Type

Playground link

Assuming the given type:

export declare type MongoManagerFilter<T> = {
  [P in keyof T]?: (P extends keyof T ? T[P] : any);
};

When trying to specify the filter:

update.$setOnInsert.createdAt = new Date();

I get this error:

Type 'Date' is not assignable to type '"createdAt" extends keyof T ? T[keyof T & "createdAt"] : any'.ts(2322)

But if I change the filter to this it works:

export declare type MongoManagerFilter<T> = {
  [P in keyof T]?: any;
};

Why does the conditional statement not actually figure out the type? It's just keeping the code as the type.

I know the filter doesn't make sense how it is, I have another type instead of keyof T to catch dot notation, but I am having the same issue with conditional statements not figuring out the actual type.

Full example

interface CollectionDocument {
  _id?: string;
  createdAt: Date;
}

type PathImpl<T, Key extends keyof T> =
  Key extends string
  ? T[Key] extends Record<string, any>
    ? `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string>}`
      | `${Key}.${Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string}`
    : never
  : never;

type Path<T> = keyof T | PathImpl<T, keyof T>;

type PathValue<T, P extends Path<T>> =
    P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? Rest extends Path<T[Key]>
        ? PathValue<T[Key], Rest>
        : never
      : never
    : P extends keyof T
      ? T[P]
      : any;

export declare type MongoManagerFilter<T> = {
    [P in Path<T>]?: PathValue<T, P>;
};

export class MongoManagerCollection<T extends CollectionDocument> {
  constructor() {}
  
  updateOne(filter: MongoManagerFilter<T>) {
    // THIS DOES NOT WORK
    filter._id = '';
    filter.createdAt = new Date();
  }
}

// THIS WORKS
let testModel = new MongoManagerCollection<CollectionDocument>();
testModel.updateOne({_id: 'test', createdAt: new Date()});

EDIT Adding another layer to this playground for more issues along same lines:

Playground link

Seems I am getting this error as well due to the recursion in the types:

Type instantiation is excessively deep and possibly infinite.ts(2589)

Upvotes: 1

Views: 283

Answers (1)

Generic type T in:

export class MongoManagerCollection<T extends CollectionDocument> {
  constructor() { }

  updateOne(filter: MongoManagerFilter<T>) {
    // THIS DOES NOT WORK
    filter._id = '';
    filter.createdAt = new Date();
  }
}

is a black box. filter._id is resolved as a non evaluated conditional type. Treat it as a non called function. Function which is has never been called does not return any value. Same is with filter._id. filter._id coresponds to this:

type PathValue<T, P extends Path<T>> =
  P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
  ? Rest extends Path<T[Key]>
  ? PathValue<T[Key], Rest>
  : never
  : never
  : P extends keyof T ////////////////////////////////////////////////
  ? T[P]              // Property is resolved as a conditional type //
  : any;              ///////////////////////////////////////////////

As you might have noticed, filter._id is P extends keyof T ? T[P] : any instead of just T[P].

There is a workaround. You can use T[P & keyof T] instead of conditional type. See working example:

interface CollectionDocument {
  _id?: string;
  createdAt: Date;
}

type PathImpl<T, Key extends keyof T> =
  Key extends string
  ? T[Key] extends Record<string, any>
  ? `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string>}`
  | `${Key}.${Exclude<keyof T[Key], keyof Date | keyof Object | keyof string> & string}`
  : never
  : never;

type Path<T> = keyof T | PathImpl<T, keyof T>;

type PathValue<T, P extends Path<T>> =
  P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
  ? Rest extends Path<T[Key]>
  ? PathValue<T[Key], Rest>
  : never
  : never
  : T[P & keyof T]

export declare type MongoManagerFilter<T> = {
  [P in Path<T>]?: PathValue<T, P>;
};

export class MongoManagerCollection<T extends CollectionDocument> {
  constructor() { }

  updateOne(filter: MongoManagerFilter<T>) {
    filter._id = ''; // ok
    filter.createdAt = new Date(); // ok
    filter.createdAt = 'sdf'; // expected error

  }
}

// THIS WORKS
let testModel = new MongoManagerCollection<CollectionDocument>();
testModel.updateOne({ _id: 'test', createdAt: new Date() });

Playground

If you are interested in path implementation, you can take a look at this answer, this utility and/or my article.

I don't know MongoDB api and the requirements of your types so I can't say whether they need to be modified or not.

Also, you can get rid of generic :

export class MongoManagerCollection {
  constructor() { }

  updateOne(filter: MongoManagerFilter<CollectionDocument>) {
    filter._id = ''; // ok
    filter.createdAt = new Date(); // ok

  }
}

// THIS WORKS
let testModel = new MongoManagerCollection();
testModel.updateOne({ _id: 'test', createdAt: new Date() });

but I don't think this is what you want.

Upvotes: 1

Related Questions