Declaration merging for Joi types

I want to narrow the type property of the ValidationErrorItem from Joi using declaration merging.

The interface in Joi looks like this:

declare namespace Joi {
   interface ValidationErrorItem {
        message: string;
        path: Array<string | number>;
        type: string;
        context?: Context;
    }
}

My index.d.ts in src/types looks like this:

import { Context } from 'joi'

declare namespace Joi {
  type ValidationErrorItemType = 'any.required' | 'date.format'
  export interface ValidationErrorItem {
    message: string
    path: Array<string | number>
    type: ValidationErrorItemType
    context?: Context
  }
}

This is my include in tsconfig.json:

 "include": ["src/**/*", "types/types.d.ts"]

However, VS Code still shows type to be of type string instead of ValidationErrorItem and when using it in a switch statement I can still use any string without getting compiler warnings.

Update

This is my tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "declaration": true,
    "target": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "paths": {
      "*": ["node_modules/*", "src/types/*"]
    }
  },
  "include": ["src/**/*", "types/joi.ts"]
}

This is my joi.d.ts based on the answer from @tymzap:

declare module 'joi' {
  // full path to joi typings is used to avoid circular import path
  import type { Context } from 'joi/lib/index'
  export { default } from 'joi/lib/index'

  type ValidationErrorItemType = 'any.required' | 'date.format'

  export type ValidationErrorItem = {
    message: string
    path: Array<string | number>
    type: ValidationErrorItemType
    context?: Context
  }
}

This is my sample usage:

error.details.map((i) => {
    const x: ValidationErrorItem = i as ValidationErrorItem
    switch (x.type) {
      case 'any.required':
        break

      default:
        break
    }
  })

Solution 1:

You can't narrow the type using interface merging. This operation extends types, not overrides them. Extending ValidationErrorItemType with string gives ValidationErrorItemType | string, that's why you still can use any string.

To achieve this you can redeclare Joi module. Modify the type to your needs and export it after reexporting original Joi typings.

joi.d.ts:

declare module 'joi' {
  // full path to joi typings is used to avoid circular import path
  import type { Context } from 'joi/lib/index';
  export * from 'joi/lib/index';
  export { default } from 'joi/lib/index';

  type ValidationErrorItemType = 'any.required' | 'date.format';

  export type ValidationErrorItem = {
    message: string;
    path: Array<string | number>;
    type: ValidationErrorItemType;
    context?: Context;
  }
}

Test:

import type { ValidationErrorItem } from 'joi';

const test: ValidationErrorItem = {
  // error: Type '"wrong.type"' is not assignable to type 'ValidationErrorItemType'.
  type: 'wrong.type',
  message: 'dummy message',
  path: ['dummy-path'],
};

This way you can effectively override given type from library, without merging with its original signature.

Edit: Joi uses old export = syntax, depending on your compiler settings this could lead to errors based on typings flavour incompatibility. In this case you can add this line to tsconfig:

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}