I have a custom type declaration file, in which I would like to override an already existing interface property (not create a new property, but modify existing)
I have tried several approaches, such as Omiting the property (shown below), with no luck.
src/types/example.d.ts
----------
import package, { AnInterface } from 'example'
interface CustomInstance extends Omit<Instance, 'clients'> {
clients: string[];
}
declare module "example-module" {
export interface Instance extends CustomInstance {
test: number
clients: CustomType
}
}
src/main.ts
-----------
example.test // number as expected
example.clients // is always the type from node_modules, not my .d.ts file
If I try to add clients without doing any extending, I getSubsequent property declarations must have the same type. Property 'clients' must be of type 'EDictionary', but here has type 'CustomType'
however if I try to extend the custom instance (as above) I get the error Type 'Instance' recursively references itself as a base type. (which even if I ts-ignore, my application will still use the type defined in node_modules).
You can't un-define something with d.ts, you can only define it more precisely.
Best you can to remove something is re-export everything from your module, changing its types
In your case you may want to not un-define clients but to narrow it down, then that will be
import { EDictionary } from 'example-module';
declare module "example-module" {
export interface Instance { // i.e. extends example-module.Instance
test: number
clients: CustomType & EDictionary // narrow it down in a compatible manner
}
}
Related
I'm working with Prisma to make a generic repository object. Here's my current interface (which the base repository implements) which works:
import { Log as LogSchema, Prisma, User as UserSchema } from "#prisma/client";
import type { CamelCase } from "type-fest";
type PrismaSchema = UserSchema | LogSchema; // I currently have to define all schemas schema.prisma here
type NonWriteable = "updatedAt" | "createdAt"; // I also have to explicitly define fields which should never be writeable
type WriteableSchemaWithId<Schema> = Omit<Schema, NonWriteable>;
type WriteableSchema<Schema> = Omit<WriteableSchemaWithId<Schema>, "id">;
interface IBaseRepository<
Key extends CamelCase<Prisma.ModelName>,
Schema extends PrismaSchema
> {
create: (parameters: { [key in Key]: WriteableSchema<Schema> }) => Promise<{
id: number;
}>;
// ... Other methods
}
However, in the type of argument to the create method are slightly incorrect. I'd really be like to using the Prisma-generated types instead of the type of the schema. For example, the Prisma client exports UserCreateArgs which defines what can be passed to prisma.user.create({ data: ... }).
Then I'd have to do this for all actions, for all schemas. Now, this isn't a problem currently because there are only two models with no relations to each other. However, I'm expecting this implementation to become brittle (and cumbersome) with a more complex database schema.
Instead, I was looking for something like:
import { Prisma } from "#prisma/client";
import type { PascalCase } from "type-fest";
type NamespaceKey<
Model extends Prisma.ModelName,
Action extends Prisma.PrismaAction
> = `${Capitalize<Model>}${PascalCase<Action>}Args`;
type ActionArgs<
Model extends Prisma.ModelName,
Action extends Prisma.PrismaAction
> = typeof Prisma[NamespaceKey<Model, Action>];
Where NamespaceKey turns User into e.g., UserCreateArgs which should be imported from the Prisma namespace.
But I get an error saying NamespaceKey cannot be used to index typeof Prisma.
Is there a way to get what I'm looking for?
Edit:
The problem seems to revolve around the inability to string-index types in a namespace.
For example, Prisma["ModelName"] works as the following is exported:
export const ModelName: {
User: 'User',
Log: 'Log'
};
Along with the type:
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
However, Prisma["UserCreateArgs"] does not seem to exist since UserCreateArgs is a type.
So, it seems like it is not possible to string-index the types defined in a namespace. If this were possible, one would assume what I have already would work.
I'm slowly converting a project to typescript and there is a dependency I'd like to add some types to via global.d.ts.
For example, that dependency has SomeClass I'd like to declare with types, but another dozen other classes I'd like to skip for now.
But, when I add types for SomeClass, it causes everything else to become errors "no exported member".
Is there a way to skip the others or declare them as any for now? Doing export type SkipThisClassForNow = any doesn't play nice with new SkipThisClassForNow().
Without any type declarations, typescript treats everything imported from that dependency as any and works. Is there a way to force typescript to behave that way? Or are type declarations like this none or all?
The example below exports Module which has index signature which will give the type any for any key that is not defined. Keys which are defined will export the defined type.
declare module 'dependency' {
class SomeClass {
constructor(a: boolean)
}
interface Module {
[key: string]: any
SomeClass: typeof SomeClass
}
const module: Module;
export = module;
}
Flow documentation only shows how to declare a custom Higher Order Component to work with custom Class and its Props. In my case I have a custom Class like:
type Props = {
navigation: Object,
isFocused: boolean
}
type State = {
config: AppConfig,
pack: Package,
init: boolean,
}
class MainAppScreen extends React.Component<Props, State> {
...
}
export default withNavigationFocus(MainAppScreen);
and want to wrap it with external HOC from 'react-navigation';
What should I do (beside //$FlowFixMe) to get rid of this message:
Error:(111, 16) Cannot build a typed interface for this module. You should annotate the exports of this module with types. Cannot determine the type of this call expression. Please provide an annotation, e.g., by adding a type cast around this expression. (signature-verification-failure)
Thanks.
UPDATE:
As #user11307804 pointed into the right direction, I have been also trying involving flow-typs for external libraries. However it still seems not to be possible to import a type for a (HOC) function like:
declare export function withNavigationFocus<Props: {...}, ComponentType: React$ComponentType<Props>>(
Component: ComponentType
): React$ComponentType<$Diff<React$ElementConfig<ComponentType>, {| isFocused: ?boolean |}>>;
When I try to import it like: (following this example)
import type {withNavigationFocus} from 'react-navigation';
I get the error:
Error:(22, 14) Cannot import the value `withNavigationFocus` as a type. `import type` only works on type exports like type aliases, interfaces, and classes. If you intended to import the type of a value use `import typeof` instead.
and if I try with typeof I get:
import typeof {withNavigationFocus} from 'react-navigation';
I get the errors:
Error:(22, 16) Cannot declare `withNavigationFocus` [1] because the name is already bound.
Error:(112, 16) Cannot call `withNavigationFocus` because Named import from module `react-navigation` [1] is not a function.
Error:(112, 16) Cannot build a typed interface for this module. You should annotate the exports of this module with types. Cannot determine the type of this call expression. Please provide an annotation, e.g., by adding a type cast around this expression. (`signature-verification-failure`)
Thanks.
Flow is complaining that withNavigationFocused is untyped. Fortunately, the flow-typed project has react-navigation types. (There are other definition files for different version of react-navigation or flow; the one I've linked is for react-navigation^4.0.0 and flow^0.114.0.) You can include the library definition in your project following the Library Definitions documentation (essentially, just save the file in <PROJECT_ROOT>/flow-typed directory).
I am using a javascript library that constructs an object like so:
usefulJS.js
function usefulJS() {}
usefulJS.protoype.doAThing = function() {
//does a thing here, returns an object
};
module.exports = new usefulJS();
It has a type definition file like so:
usefulJS/index.d.ts
export class usefulJS {
public doAThing(): object;
}
And it is used in the following typescript file:
myTypescript.ts
import {randomOtherThing,usefulJS} from "library-that-includes-usefulJS";
const myObj = usefulJS.doAThing();
But I get a red underline under .doAThing() in my typescript file with the following message:
"Property 'doAThing' does not exist on type 'typeof usefulJS'."
When I run the code with //#ts-ignore over the function call it works, so it definitely seem to be a typescript issue and not a javascript issue.
Am I doing something wrong with my imports? How should I import an object that is already constructed, as opposed to a prototype for an object?
Or is there something wrong with the library's type definitions?
If it is any help, there is another project in non-typescript node that uses this library. Its import line looks like this:
const usefulJS = requre("library-that-includes-usefulJS").usefulJS;
Just a guess: TypeScript probably locate usefulJS from usefulJS.js instead of index.d.ts (you can check if that's the case by using "go to definition" in VS Code). If that's the case TypeScript will not recognize that usefulJS has a doAThing method, because it is assigned through prototype. You can probably solve this by adding this to your tsconfig.json:
"compilerOptions": {
"baseUrl": "./",
"paths": {
"library-that-includes-usefulJS": [
"./usefulJS/index.d.ts"
]
}
}
By making library changes I am able to get it to work.
Basically, the index.d.ts file doesn't indicate that helpfulJS.js exports a value. Unfortunately, we can't indicate that it exports the value helpfulJS, because that would result in a name overlap with the type. So helpfulJS.js needs to be modified to export a different variable name.
usefulJS.js
function usefulJS() {}
usefulJS.protoype.doAThing = function() {
//does a thing here, returns an object
};
let usefulJSObj = new usefulJS();
module.exports = usefulJSObj;
usefulJS/index.d.ts
export class usefulJS {
public doAThing(): object;
}
export const usefulJSObj : usefulJS;
myTypescript.ts
import {randomOtherThing,usefulJSObj} from "library-that-includes-usefulJS";
const myObj = usefulJSObj.doAThing();
I do have the ability to change this library in this case but I am loath to do so because it has impact on other projects.
I am still hoping there is a way to fix this without changing the library (or maybe only changing the type file, since the other users of the library are javascript).
I found a way to change the library that solves the problem without making any of the existing javascript clients change their code.
Basically, by changing the library to export a type with static functions instead of an unnamed object with object functions, typescript stops complaining about being unable to call the method and the javascript calls to the library don't change at all.
(usefulJS.doAThing() with usefulJS as an object calling its prototype function changes to usefulJS.doAThing() as a type calling its static function, with javascript not caring about the difference.)
usefulJS.js
class usefulJS() {
static doAThing() {
//does a thing here, returns an object
};
}
//Returns a type with a static method instead of a constructed object
module.exports = usefulJS;
usefulJS/index.d.ts
export class usefulJS {
public static doAThing(): object;
}
myTypescript.ts
import {randomOtherThing,usefulJS} from "library-that-includes-usefulJS";
const myObj = usefulJS.doAThing();
Still doesn't help much for those who don't have control over the library, unfortunately, but at least we don't cause exploding changes to all the javascript clients that already worked just to make typescript clients work.
I'm getting the following error:
src/models/priceAdjustment.ts(55,2): error TS2345: Argument of type '{ as: string; foreignKey: string; gql: any; }' is not assignable to parameter of type 'AssociationOptionsHasMany'.
Object literal may only specify known properties, and 'gql' does not exist in type 'AssociationOptionsHasMany'.
The error is obvious, I'm trying to assign a "gql" key which doesn't exist (I'm attaching it on so I can access it later on in my code somewhere else).
What I'd like to do, is extend the model.hasMany interface here, and add my own key. Is this possible? If so, how do I?
My current workaround is to cast the object to <any> before passing it in, which feels like a dirty workaround.
I have attempted the following:
import Sequelize from 'sequelize';
export declare module Sequelize {
export interface MyAssociationOptionsHasMany extends Sequelize.AssociationOptionsHasMany {
gql?: any
}
export interface MyAssociations extends Sequelize.Associations {
hasMany(target: Sequelize.Model<any, any>, options?: MyAssociationOptionsHasMany): void;
}
}
And now I get the following error:
TypeError: Cannot read property '239' of undefined
at getDeclarationSpaces (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:33121:54)
at checkExportsOnMergedDeclarations (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:33067:41)
at checkModuleDeclaration (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:35036:17)
at checkSourceElement (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:35505:28)
at Object.forEach (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:275:30)
at checkSourceFileWorker (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:35566:20)
at checkSourceFile (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:35551:13)
at getDiagnosticsWorker (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:35604:17)
at Object.getDiagnostics (C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:35593:24)
at C:\Users\user\AppData\Roaming\npm\node_modules\typescript\lib\tsc.js:56188:85
You can do this via Declaration Merging. The best way I have found this to work is by importing the library into an "extension" file and extending the library from there. For instance,
extensions/library-name.extension.ts:
import AssociationOptionsHasMany from '/path/to/definition';
import 'library-name';
declare module 'library-name' {
interface InterfaceToExtend {
hasMany(options: AssociationOptionsHasMany): ReturnType;
}
}
In the above example, we want to extend an Interface created in the library-name module. Since we are using imports, TypeScript automatically creates a module for the file, so we have to tell TypeScript that we want to extend the interface in the library-name module, not the module created for this file. To do that, we simply declare the module using the same name as the library we imported.
After telling TypeScript in which module the interface should be extended, we just use Declaration Merging by defining an interface with the same name as the interface that should be extended and adding whatever we need to it. In this case, that would be a new method that takes as an argument the specific type we want to pass it.
I have also had to update the tsconfig.json file so the extensions would be loaded first. You can do that by including the extensions directory before the rest of the app:
{
/* Blah, settings, blah */
includes: ['./app/extensions/*.ts', './app/**/*.ts'],
/* Blah, settings, blah */
}
If you're using Sublime, it might complain still, but actually compiling the code or exiting out of Sublime and reopening it seems to work just fine.
A word of warning. Declaration Merging is powerful, but can result in unexpected behavior. In general, try to avoid extending components you (or someone else) have defined elsewhere. If may be a sign that you are using these components incorrectly or the need for refactoring. Remember, the benefit of TypeScript is strict(er) typing.