Use imports inside interface definition - javascript

I'm having a strange problem with typescript interfaces. Because I'm using mongoose models I need to define one, but for some reason it's not recognising things that I have explicitly imported. This part works fine:
export interface ITrip extends mongoose.Document {
//
}
export var TripSchema = new mongoose.Schema({
//
});
export var Trip = mongoose.model<ITrip>('Trip', TripSchema);
Now, I'm defining another interface, that has an array of Trip. I need this for subdocuments.
import {Trip, ITrip} from '../trips/trip.model';
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [Trip]
}
}
The TS compiler gives this error: feed.ts(12,13): error TS2304: Cannot find name 'Trip'. (referring to trips: [Trip]). It doesn't say that the import failed or anything. I can even use trip inside the same file to create new objects var a = new Trip({}); without problem. Inside the interface it breaks.

Trip isn't a type, it's a variable, so you can do this:
let t = Trip;
let t2 = new Trip({});
But you can't do this:
let t: Trip;
You should change it to typeof Trip:
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [typeof Trip]
}
}
Also, if you want IFeed.lastSnapshot.trips to be an array, then it should be:
trips: typeof Trip[]
What you declared is a tuple of one item.
Edit
With an object the assignment is always the same (both js and ts):
let o = {
key: "value"
}
But when declaring types in typescript then you're not dealing with values:
interface A {
key: string;
}
let o: A = {
key: "value"
}
In the mongoose documentation they are using only javascript so all of their examples don't include the type declarations.

Related

How to map my string key to some possible enum values?

i have the following interface
export interface AuctionMOLQueryParams {
filterAuctionDirection?: string;
}
because filterAuctionDirection can be one of three possible values i setted to be enum
export enum ProductDirection {
upwards = 'A01',
downwards = 'A02',
all = 'All'
}
export interface AuctionMOLQueryParams {
filterAuctionDirection?: ProductDirection;
}
the problem is that now when i try to use this object type
let queryParams: AuctionMOLQueryParams = {
filterAuctionDirection: ProductDirection.all,
}
Types of property 'filterAuctionDirection' are incompatible.
Type 'string' is not assignable to type 'ProductDirection'strong text
how can i solve this ? How can i map my filterAuctionDirection to be one of this three values and the type safey will still work correctly ?
TS2717: Subsequent property declarations must have the same type.
Property 'filterAuctionDirection' must be of type 'string', but here has type 'ProductDirection'.
If I understood your question correctly, you're defining the interface AuctionMOLQueryParams twice.
Initially you define the field filterAuctionDirection as string but later you're trying to mark it as ProductDirection.
If you're trying narrow a variable type of an externally declared interface (in this example AuctionMOLQueryParams) you have to define a new interface extending of it.
Example for your use case:
export interface AuctionMOLQueryParams {
filterAuctionDirection?: string;
}
export enum ProductDirection {
upwards = 'A01',
downwards = 'A02',
all = 'All'
}
export interface MyAuctionMOLQueryParams extends AuctionMOLQueryParams {
filterAuctionDirection?: ProductDirection;
}
let queryParamsValid1: MyAuctionMOLQueryParams = {
filterAuctionDirection: ProductDirection.all,
}
let queryParamsInvalid2: MyAuctionMOLQueryParams = {
filterAuctionDirection: "My",
}
let queryParamsInvalid1: MyAuctionMOLQueryParams = {
filterAuctionDirection: "All",
}

Question concerning types, Maps and subclasses in typescript

I have a Chart class which has several subclasses (BarChart, TimeseriesChart...) which extend Chart.
I use a method called buildChart to build these charts. It maps the enum ChartsEnum (for example stackedTimeseries or barChart) to the correct class using a Map:
export function buildChart(type: Charts, data: Array<TimeseriesData>) {
var chartsMap = new Map<Charts, InstantiableAbstractClass<typeof Chart>([
[Charts.stackedTimeseries, TimeseriesChart],
[Charts.barChart, BarChart],
])
const chart = chartsMap.get(type)
return new chart(data).chart;
}
The type InstantiableAbstractClass looks like this:
export declare type InstantiableClass<T> = (new ( ...args: any) => { [x: string]: any }) & T;
For example, the class and constructor of TimeseriesChart looks like this:
export class TimeseriesChart extends Chart{
constructor(data: Array<TimeseriesData>) {
super(data);
}
}
I now want to add a second attribute to the chart-class called options (next to the existing attribute data).
The problem is now that options requires for each ChartType (BarChart, TimeseriesChart) different properties.
For example BarChart requires these properties:
{
start: number;
end?: number;
}
and TimeseriesChart requires a type like this:
{
description: string;
}
The constructor of TimeseriesChart would then look like this:
export class TimeseriesChart extends Chart{
constructor(data: Array<TimeseriesData>, options: TimeseriesChartOptions) {
super(data, options);
}
}
This means that the method buildChart needs a new argument options which can then be passed to the specific classes (as done with the argument data).
What is the best way for doing this? I thought of using generics and then defining n types of types for options for n subclasses, but I could not figure out on how to change the type InstantiableAbstractClass correctly for that.
You can find a complete example with some descriptions here.
I really appreciate your help! If you need any further information I would be happy to provide them.
Thank you and all the best
Lukas
Firstly, it seems like you want your Chart class to be abstract since you are only ever going to use concrete subclasses of it, and you want the buildChart() method to throw if not overridden. I'm also going to use parameter properties as a shorthand for declaring a field and an idential constructor parameter and assigning the latter to the former. I'm adding an options parameter of the any type because we don't really need any type checking in the base class like this.
abstract class Chart {
constructor(protected data: Array<TimeseriesData>, protected options: any) { }
abstract buildChart(): void;
}
Anyway, for the subclasses, we will narrow the options property to the appropriate type by using the declare property modifier, and specify that type in the constructor parameter also:
interface BarChartOptions {
start: number,
end: number
}
class BarChart extends Chart {
declare options: BarChartOptions;
constructor(data: Array<TimeseriesData>, options: BarChartOptions) {
super(data, options);
}
buildChart() { return {}; }
protected getChartData() { }
}
interface TimeseriesChartOptions {
description: string
}
class TimeseriesChart extends Chart {
declare options: TimeseriesChartOptions;
constructor(data: Array<TimeseriesData>, options: TimeseriesChartOptions) {
super(data, options);
}
buildChart() { return { }; }
protected getChartData() { }
}
Now we need to express the relationship between your Charts enum and the different subclasses. Let's make a chartConstructors object holding the enums as keys and the constructors as values:
const chartConstructors = {
[Charts.stackedTimeseries]: TimeseriesChart,
[Charts.barChart]: BarChart
}
You should add an entry to that for each subclass of Chart you care about.
Finally we will write your buildChart() function. Let's make some helper types:
type ChartConstructorParameters<C extends Charts> =
ConstructorParameters<typeof chartConstructors[C]>;
type ChartInstance<C extends Charts> =
InstanceType<typeof chartConstructors[C]>;
type ChartConstructor<C extends Charts> =
new (...args: ChartConstructorParameters<C>) => ChartInstance<C>;
The ChartConstructorParameters<C> type will take an enum member as C and turn it into the tuple of arguments to the relevant class constructor, using the ConstructorParameters<T> utility type.
The ChartInstance<C> type does the same thing for the instance type of the relevant class constructor, using the InstanceType<T> utility type.
And finally the ChartConstructor<C> represents the relevant constructor signature for the class.
And now here's buildChart():
function buildChart<C extends Charts>(
type: C, ...ctorArgs: ChartConstructorParameters<C>
) {
const chartConstructor = chartConstructors[type] as ChartConstructor<C>;
return new chartConstructor(...ctorArgs);
}
It's a generic function which takes a type parameter of generic type C corresponding to an enum member. And for the remaining arguments, it takes whatever the constructor parameters of the relevant class is. This will be a pair of data and options for the BarChart and TimeseriesChart, but if you add subclasses with other constructor parameters, it will accept those.
The output is an instance of the class, which it constructs by grabbing the relevant constructor from chartConstructors (we need to use a type assertion to convince the compiler that chartConstructors[type] is actually of the type ChartConstructor<C>; it's something the compiler is unable to see due to its inability to reason too much about unspecified generic types like C).
So, does it work?
const barChart = buildChart(Charts.barChart, [], { start: 1, end: 2 });
// const barChart: BarChart
const timeSeriesChart = buildChart(Charts.stackedTimeseries, [], { description: "" });
// const timeSeriesChart: TimeseriesChart
Yes, looks good!
Playground link to code

Type declaration file doesn't work with commonjs module syntax

Today I was writing a type declaration file for a JavaScript file but despite my hours of trying I couldn't make it work inside a node application. Then I tried switching to es6 module syntax and surprisingly it worked.
Then I discovered that I can also make it work with commonjs module syntax if I add ".default" before accessing any properties of that module. For example I've to use person.default.name instead of person.name to get the intellisence.
I wrote a small module to demonstrate the problem. I've created an object with identical typings of my actual module.
index.js
const names = {
firstname: "Alex",
middlename: "Blex",
lastname: "Clex",
};
const state = {
isAlive() {
return true;
},
isWalking() {
return false;
},
};
function talk(speech) {
console.log(speech);
}
const person = {
names,
state,
talk
};
module.exports = person;
index.d.ts
declare type predicate = (v: unknown) => boolean;
declare function talk(speech: string): void;
declare const person: {
names: {
[key: string]: string;
};
state: {
[key: string]: predicate;
};
talk: typeof talk;
};
export default person;
test1.js
const person = require("./index");
const isAlive = person.default.state.isAlive();
//---------------------^^^^^^^----------------
// The above line throws an error as expected.
// I've to use "person.default.whatever" to get intellisence
// In the editor it shows "typeof isAlive = boolean".
const isReallyAlive = person.state.isAlive();
// EditorError: Property 'state' does not exist on type
// 'typeof import(./index.js)'.
// In the editor it shows typeof "isReallyAlive = any"
// But infact isReallyAlive is boolean.
test2.js
Using es6 module syntax it works perfectly.
I'm fairly new to Typescript so kindly give me some hint where I'm making the mistake.
Thanks in advance, I highly appreciate your time on StackOverflow <3.
So as explained by this post https://stackoverflow.com/a/40295288/10315665 the export default option only creates a default key inside of the exports object. That's why you can still have normal exports besides it.
Many people consider module.exports = ... to be equivalent to export default ... and exports.foo ... to be equivalent to export const foo = .... That's not quite true though, or at least not how Babel does it.
So your definition file is wrong. It should be:
declare type predicate = (v: unknown) => boolean;
export declare const names: {
[key: string]: string;
};
export declare const state: {
[key: string]: predicate;
};
export declare function talk(speech: string): void;
And if you respect that, you can actually utilize typescript and it's awesome type checking by simply writing typescript in the first place!:
export const names = {
firstname: "Alex",
middlename: "Blex",
lastname: "Clex",
};
export const state = {
isAlive() {
return true;
},
isWalking() {
return false;
},
};
export function talk(speech: string) {
console.log(speech);
}
Just remember to enable "declaration": true in the tsconfig to also generate declaration files 😉.

How to make sure a typescript module conforms to an interface

I'm kind of writing a specific content scraper for some domains. So for those domains supported I just have some functions declared, let's just say there is a
export function getContent(html: string): string {}
So, I have many files with this exact interface for example ...
export interface Domain {
supportsHttps: boolean;
getContent(html: string): string;
}
And then for simplicity's sake (to make a map of supported hostname and my domains file), I just
// domainsList.ts
import * as domainA from './domains/domainA';
export default {
"www.domainA.com": domainA,
}
Then I import my domain list
// index.ts
import * as url from 'url';
import domains from './domainsList';
const maybeDomain: Domain | undefined = domains[url.parse(someUrl).host];
if (maybeDomain) {
// here I get proper autocompletion ...
const content = maybeDomain.getContent(someHtml);
} else {
throw new Error('domain not supported');
}
But if I refactor the name of the function in the interface from getContent to getContents for example, I actually do not have any compilation error from inside all the domains files.
I want to ensure ./domains/domainA.ts exported structure conforms to my Domain interface. Do I have a way to do that ?
Since you are not actually defining the function have the type of Domain the compiler won't link the two of them together, thus no errors.
Also, the Domain interface suits a class much better than a function.
You can get all the checks by defining classes instead of functions. Like this:
class AwesomeDomain implements Domain {
public supportsHttps: boolean;
getConten(html: string): string {
return '';
}
}
You can find a full example here.
This is my solution, as long as you expose all of your imports exclusively via an index file export.
module/index.ts
import * as A from "./A";
import * as A from "./B";
// type definitions
export type MyFunc = (args: any[]) => any;
export type MyCollection = {
[key: string]: MyModule,
}
export interface MyModule {
KEY: string;
EXAMPLE_ARRAY: string[];
someFn: MyFunc;
}
export const COMMANDS = {
[A.KEY]: A,
[B.KEY]: B,
} as MyCollection;
module/A.ts
import { MyFunc } from ".";
export const KEY = "some-key";
export const EXAMPLES = [
"hello",
"world",
]
export const someFn: CommandFn = async (args: any[]) => {
return ''
};
If you uncomment one of the exports:
Conversion of type '{ "A": typeof A; "B": typeof B; }' to type 'MyCollection' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Property '[A.KEY]' is incompatible with index signature.
Property 'someFn' is missing in type 'typeof import("...") but required in type 'MyModule'.

How to reduce javascript object to only contain properties from interface

When using typescript a declared interface could look like this:
interface MyInterface {
test: string;
}
And an implementation with extra property could be like this:
class MyTest implements MyInterface {
test: string;
newTest: string;
}
Example (here the variable 'reduced' still contain the property 'newTest'):
var test: MyTest = {test: "hello", newTest: "world"}
var reduced: MyInterface = test; // something clever is needed
Question
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Why
The problem occur when trying to use the 'reduced' variable with angular.toJson before sending it to a rest service - the toJson method transforms the newTest variable, even if it's not accessible on the instance during compile, and this makes the rest service not accept the json since it has properties that shouldn't be there.
It is not possible to do this. The reason being interface is a Typescript construct and the transpiled JS code is empty
//this code transpiles to empty!
interface MyInterface {
test: string;
}
Thus at runtime there is nothing to 'work with' - no properties exist to interrogate.
The answer by #jamesmoey explains a workaround to achieve the desired outcome.
A similar solution I use is simply to define the 'interface' as a class -
class MyInterface {
test: string = undefined;
}
Then you can use lodash to pick the properties from the 'interface' to inject into you object:
import _ from 'lodash'; //npm i lodash
const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property
see JSFiddle
This is a pragmatic solution that has served me well without getting bogged down on semantics about whether it actually is an interface and/or naming conventions (e.g. IMyInterface or MyInterface) and allows you to mock and unit test
TS 2.1 has Object Spread and Rest, so it is possible now:
var my: MyTest = {test: "hello", newTest: "world"}
var { test, ...reduced } = my;
After that reduced will contain all properties except of "test".
Another possible approach:
As other answers have mentioned, you can't avoid doing something at runtime; TypeScript compiles to JavaScript, mostly by simply removing interface/type definitions, annotations, and assertions. The type system is erased, and your MyInterface is nowhere to be found in the runtime code that needs it.
So, you will need something like an array of keys you want to keep in your reduced object:
const myTestKeys = ["test"] as const;
By itself this is fragile, since if MyInterface is modified, your code might not notice. One possible way to make your code notice is to set up some type alias definitions that will cause a compiler error if myTestKeys doesn't match up with keyof MyInterface:
// the following line will error if myTestKeys has entries not in keyof MyInterface:
type ExtraTestKeysWarning<T extends never =
Exclude<typeof myTestKeys[number], keyof MyInterface>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_EXTRA_KEY_NAMES_HERE' does not satisfy the constraint 'never'
// the following line will error if myTestKeys is missing entries from keyof MyInterface:
type MissingTestKeysWarning<T extends never =
Exclude<keyof MyInterface, typeof myTestKeys[number]>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_MISSING_KEY_NAMES_HERE' does not satisfy the constraint 'never'
That's not very pretty, but if you change MyInterface, one or both of the above lines will give an error that hopefully is expressive enough that the developer can modify myTestKeys.
There are ways to make this more general, or possibly less intrusive, but almost no matter what you do, the best you can reasonably expect from TypeScript is that your code will give compiler warnings in the face of unexpected changes to an interface; not that your code will actually do different things at runtime.
Once you have the keys you care about you can write a pick() function that pulls just those properties out of an object:
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
return keys.reduce((o, k) => (o[k] = obj[k], o), {} as Pick<T, K>);
}
And them we can use it on your test object to get reduced:
var test: MyTest = { test: "hello", newTest: "world" }
const reduced: MyInterface = pick(test, ...myTestKeys);
console.log(JSON.stringify(reduced)); // {"test": "hello"}
That works!
Playground link to code
Are you trying to only set/assign properties listed on the interface only? Functionality like that is not available in TypeScript but it is very simple to write a function to perform the behaviour you looking for.
interface IPerson {
name: string;
}
class Person implements IPerson {
name: string = '';
}
class Staff implements IPerson {
name: string = '';
position: string = '';
}
var jimStaff: Staff = {
name: 'Jim',
position: 'Programmer'
};
var jim: Person = new Person();
limitedAssign(jimStaff, jim);
console.log(jim);
function limitedAssign<T,S>(source: T, destination: S): void {
for (var prop in destination) {
if (source[prop] && destination.hasOwnProperty(prop)) {
destination[prop] = source[prop];
}
}
}
In your example newTest property won't be accessible thru the reduced variable, so that's the goal of using types. The typescript brings type checking, but it doesn't manipulates the object properties.
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Since TypeScript is structural this means that anything that contains the relevant information is Type Compatible and therefore assignable.
That said, TypeScript 1.6 will get a concept called freshness. This will make it easier to catch clear typos (note freshness only applies to object literals):
// ERROR : `newText` does not exist on `MyInterface`
var reduced: MyInterface = {test: "hello", newTest: "world"};
Easy example:
let all_animals = { cat: 'bob', dog: 'puka', fish: 'blup' };
const { cat, ...another_animals } = all_animals;
console.log(cat); // bob
One solution could be to use a class instead of an interface and use a factory method (a public static member function that returns a new object of it's type). The model is the only place where you know the allowed properties and it's the place where you don't forget to update them accidentaly on model changes.
class MyClass {
test: string;
public static from(myClass: MyClass): MyClass {
return {test: myClass.test};
}
}
Example:
class MyTest extends MyClass {
test: string;
newTest: string;
}
const myTest: MyTest = {test: 'foo', newTest: 'bar'};
const myClass: MyClass = MyClass.from(myTest);

Categories