So I have a provider class
export class Provider {
activeAccount: boolean;
description: String;
name: String;
providerType: any;
publicUrl: String;
uuid: String;
isValidStructure(obj){
let keys = Object.keys(obj), valid = true,
properties = ['activeAccount', 'description', 'name', 'providerType', 'publicUrl', 'uuid']
keys.forEach((key)=>{
if(!properties.includes(key)){
valid = false
}
})
return valid
}
}
I have a method to validate that the data used to create the model is a property of the model, it works but is there a generic way of doing this
The properties array is hardcoded in the function isValidStructure
let validData: any = {name: 'some name'}
let invalidData: any = {namexx: 'the key is invalid'}
let provider = Object.assign(new Provider(), validData)
let provider2 = Object.assign(new Provider(), invalidData)
provider.isValidStructure(validData)//true
provider2.isValidStructure(invalidData)//false
Above is some examples of the function and its output
Is there some way of getting the properties of the class
There is no way to get a list of properties of the defined-class in javascript or out-of-the-box solution for typescript, you have to manually inject it.
export class Provider {
static properties = [...]
}
/// or
Provider.properties = []
You could define an interface that has all the common properties between the class and your passed in config obj and then have your class implement it and your constructor take that type as a param.
export interface IProvider {
activeAccount?: boolean;
description?: String;
name?: String;
providerType?: any;
publicUrl?: String;
uuid?: String;
}
export class Provider implements IProvider {
activeAccount: boolean;
description: String;
name: String;
providerType: any;
publicUrl: String;
uuid: String;
constructor(obj?: IProvider) {
if (obj) {
this.activeAccount = obj.activeAccount;
...
}
}
}
const provider = new Provider({...});
UPDATE
If none/any of the properties are required, you could add a ? in the interface between the prop name and colon. That way you can allow the constructor obj have as many of the valid props as it needs, but it would still raise an error if you try to pass in:
const provider = new Provider({ invalid: true });
Take a look at Typescript Interfaces for more info
UPDATE 2
To put this back into your original example:
let validData: any = {name: 'some name'}
let invalidData: any = {namexx: 'the key is invalid'}
let provider = Object.assign(new Provider(), validData)
let provider2 = Object.assign(new Provider(), invalidData)
Defining both validData and invalidData as any will force Typescript not to yell at you with red squigglies because it basically says "this might be an IProvider". So you should set validData: IProvider instead of any.
Also, the same thing happens with Object.assign() when adding props to a class instance. It appears that Typescript doesn't mind that you're attempting to put in invalid props if you do this:
const validData: IProvider = { name: 'some name' };
const invalidData: any = { namexx: 'the key is invalid' };
const provider: Provider = Object.assign(new Provider(validData), invalidData);
// Provider { name: 'some name', namexx: 'the key is invalid' }
However, if you again change any to IProvider on invalidData: IProvider, it'll yell at you for the namexx prop in there.
That all said, since typing and validating seems to be important to you, then you should try to ensure that all types are explicitly declared to allow Typescript to do what it does.
Related
I am working on a Node Js (TypeScript) architecture and for some reason, I want to bind my interface to a specific object. I am making a general class that is extended by other subclasses and it will have a very general code. So my code looks like
interface User {
name: string;
}
interface Profile {
title: string;
}
class Parent {
name: string;
interface: Interface; // Help required here, getting error can't use type as a variable
constructor( name, interface ) {
// Load schema and store here
this.name = name
this.interface = interface
}
// Though this is not correct I hope you get the idea of what I am trying to do
get (): this.interface {
// fetch the data and return
return data
}
set (data: this.interface): void {
// adding new data
}
}
class UserSchema extends Parent {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent {
// Class with custom functions for ProfileSchema
}
// Config file that saves the configs for different modules
const moduleConfig = [
{
name: "User Module",
class: UserSchema,
interface: User
},
{
name: "Profile Module",
class: ProfileSchema,
interface: Profile
},
]
const allModules = {}
// Loading the modules
moduleConfig.map(config => {
allModules[config.name] = new config.class(
config.name,
config.interface
)
})
export allModules;
I need suggestions on how should I bind my interfaces with their respective configs. Till now I have had no luck with that.
PS: All this code is separated into their respective files.
This is the use case for generics. You can even see them as "variable for types".
Instead of having an interface property in your Parent class, the latter would have a generic type:
class Parent<T> { // T is the generic type
name: string;
// interface: Interface; // generic is already provided at class level
constructor( name ) {
// Load schema and store here
this.name = name
}
get (): T {
// fetch the data and return
return data
}
set (data: T): void {
// adding new data
}
}
// Here you specify the concrete generic type
class UserSchema extends Parent<User> {
// Class with custom functions for UserSchema
}
class ProfileSchema extends Parent<Profile> {
// Class with custom functions for ProfileSchema
}
I am trying to create a class or constructor function, which I specify a list of values to be used in the type of one of it's methods.
As an example I have this code.
const MyAnimal = function (animal: string[]) {
type Animal = typeof animal[number]
this.getAnimal = (url: Animal) => {
console.log(url)
}
}
const animalTest = new MyAnimal(['sheep', 'dog', 'cat'])
// I would like this to fail as 'mouse' is not part of the array ['sheep', 'dog', 'cat']
animalTest.getAnimal('mouse')
I want getAnimal to have the type 'sheep' | 'dog' | 'cat' and for the intellisense to warn me if I add something different
Is this possible?
You can do that via a generic type parameter and a readonly array of animals, like this;
class MyAnimal<Animal> {
constructor(public animals: readonly Animal[]) {
}
getAnimal(url: Animal) {
console.log(url);
}
}
const animalTest = new MyAnimal(['sheep', 'dog', 'cat'] as const);
animalTest.getAnimal('mouse'); // Error as desired
animalTest.getAnimal('sheep'); // Works
Playground link
TypeScript can infer the string literal union type to provide as the type argument for the Animal type parameter because the array is readonly (which we satisfy when calling the constructor via as const), so TypeScript knows it won't change at runtime.
In order to infer literal type, you can use variadic tuple types:
type Animal<Name extends string> = {
name: Name,
url: `https://${string}`
}
class MyAnimal<
Name extends string,
Item extends Animal<Name>,
List extends Item[]
> {
constructor(public animals: [...List]) {
}
// You should get "url" from infered list and not from Item["url"]
getAnimal<Url extends List[number]['url']>(url: Url) {
console.log(url);
}
}
const animalTest = new MyAnimal(
[
{ name: 'sheep', url: 'https://sheep.com', },
{ name: 'dog', url: 'https://dog.com', },
{ name: 'cat', url: 'https://cat.com', }
]);
animalTest.getAnimal('https://dog.com'); // ok
animalTest.getAnimal('https://mouse.com'); // expected error
Playground
If you want to learn more about literal type inference, you can check my article. The goal is to provide as much as possible constraints to generic parameter. If there are enough constraints TS will infer literal type.
This might a be relatively noob question,
I have an interface
interface Employee {
name: string
}
and I would like to have an extended version of this after it being saved into the DB:
interface EmployeeDb {
id: string,
name: string
}
I would like to differentiate it when handling checks so after saving data in my storage, the type checker won't complain about not having id value. Meaning I want to avoid using this:
interface Employee {
id?: string,
name: string
}
so I don't have to check for id everywhere.
So I am trying to do it this way:
type Employee = {
name: string
}
type IDatabaseObject<T> = {
id: IDatabaseObjectId;
[P in keyof T]: T[P];
};
type EmployeeDb = IDatabaseObject<Employee>
which the IDE gives an error with the top syntax
A computed property name must be of type 'string', 'number', 'symbol',
or 'any'.ts(2464)
so I tried to use interface and extend it
interface IDatabaseObject {
id: string
}
interface EmployeeDb extends Employee, IDatabaseObject {}
but in the backend code when I try to use this setup I get an error from vscode eslint again. I have a small code here that adds the data to localstorage, generates a id and returns the data. see code:
class DbAsyncStorageTemplate<
InputDataType,
OutputDataType extends IDatabaseObject
> {
async addEntry(object: InputDataType): Promise<OutputDataType> {
const id: string = generateUuid()
const dbObject = { id, ...object }
dbObject.id = id
// add the item to AsyncStorage directly
await AsyncStorage.setItem(id, JSON.stringify(object))
// ERROR HERE: return the new object
return dbObject as OutputDataType
}
}
}
but I get an error from the IDE (eslint) for the last line
Conversion of type '{ id: string; } & InputDataType' to type
'OutputDataType' may be a mistake because neither type sufficiently
overlaps with the other. If this was intentional, convert the
expression to 'unknown' first. '{ id: string; } & InputDataType' is
assignable to the constraint of type 'OutputDataType', but
'OutputDataType' could be instantiated with a different subtype of
constraint 'any'.
any recommendation on how to do this properly?
I believe you're looking for intersections of types.
type Employee = {
name: string
}
type EmployeeDb = {
id: string;
} & Employee;
You could also define the raw DB interface and use Pick or Omit utilities as needed.
Pick Utility
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
I think you are looking for this: https://www.typescriptlang.org/docs/handbook/advanced-types.html#:~:text=an%20intersection%20type%3A-,//%20Use%20this%3A,%7D,-Try
You are trying to create a new type (IDatabaseObject) based on an old type (Employee, for instance; or T, in the generic case). This is a Mapped Type.
In your code,
[P in keyof T]: T[P]
returns your old type rather than members of that old type. So you need to close the curly brackets and intersect it with any other new members you want to add.
i.e. do the following for IDatabseObject
type IDatabaseObject<T> = {
id: number;
} & {
[P in keyof T]: T[P];
};
With the Vue composition API we created the following composable:
import { computed, reactive, SetupContext, ref } from '#vue/composition-api'
export const useApplications = (root: SetupContext['root']) => {
const applications = reactive({
1: {
name: ref(root.$t('app1.name')),
description: ref(root.$t('app1.description')),
formName: 'app1form',
},
2: {
name: ref(root.$t('app2.name')),
description: ref(root.$t('app2.description')),
formName: 'app2form',
},
})
const getApplication = (id: string) => {
return applications[id]
}
return {
applications: computed(() => applications),
getApplication,
}
}
Although the code works fine it generates the TS error:
#typescript-eslint/no-unsafe-return: Unsafe return of an any typed value
When hovering over the applications section it's clear that typescript recognizes the types except for the property name (1):
Do we need to create an interface to solve this and do we have to redefine each and every property in the interface? I tried something like this but it is incorrect:
interface IApplication {
[key]: string {
name : string
description: string
formName: string
}
}
TypeScript doesn't generally type things so they can be indexed by any string key. As you say, you can define an interface for it:
interface IApplications {
[key: string]: {
name : string;
description: string;
formName: string;
};
}
// ...
const applications: IApplications = ...
Or you might just use a type for the object part of that and use the built-in Record type for applications:
interface IApplication {
name : string;
description: string;
formName: string;
}
// ...
const applications: Record<string, IApplication> = ...
Or combining the two:
interface IApplication {
name : string;
description: string;
formName: string;
}
type IApplications = Record<string, IApplication>;
(Or you can inline the IApplication part. Or... :-) )
According to the structure of the parameters of reactive function, the interface could be defined like :
interface IApplication {
[key:string]:{
name : string
description: string
formName: string
}
}
and
const applications = reactive<IApplication>({ ...
But I want to suggest another approach which define tha applications as reactive parameter which has an array as value:
import { computed, reactive, SetupContext, ref ,toRef} from '#vue/composition-api'
interface IApplication {
name : string
description: string
formName: string
}
export const useApplications = (root: SetupContext['root']) => {
const state= reactive<Array<IApplication>>({ applications :
[{
name: ref(root.$t('app1.name')),
description: ref(root.$t('app1.description')),
formName: 'app1form',
},
{
name: ref(root.$t('app2.name')),
description: ref(root.$t('app2.description')),
formName: 'app2form',
}],
})
const getApplication = (index: number) => {
return state.applications[index]
}
return {
applications: toRef(state,'applications'),
getApplication,
}
}
I have an API that returns JSON data.
The returned data is not structured as I want so I have to change it.
{
"#odata.context":"xxxxxx",
"id":"xxxxxxxx",
"businessPhones":[
],
"displayName":"name",
"givenName":"pseudo",
"jobTitle":null,
"mail":"hamza#mail.co",
"mobilePhone":null,
"officeLocation":null,
"preferredLanguage":"fr-FR",
"surname":"Hadda",
"userPrincipalName":"hamza#mail.co"
}
Here's my interface
export interface UserInfos {
odataContext: string;
id: string;
businessPhonesNumbers: any[];
fullName: string;
givenName: string;
jobTitle: any;
mail: string;
mobilePhoneNumber: any;
office: any;
Language: string;
surname: string;
userPrincipalName: string;
}
I would like to know what is the well optimized way to intercept data and place is in my object. Should I create a class and pass API response in the constructor to strucrure my data or is it possible to do it with TS interfaces?
There should be a single place when JSON data is being converted to the App data. Depends on your App architecture it may be for example some user component, user service or user model. Speaking of services, I see this logic as a part of UserService (where the App deals with all User functionality) or even UserInfoService (a sub-service for handling only UserInfo stuff). Skipping user-component approach I would like to draft some thoughts on user-model approach (personally I like heavy models):
user-info.interface.ts
export interface IUserInfo {
odataContext: string;
id: string;
businessPhoneNumbers: any[];
// ...
}
export interface IUserInfoJson {
'#odata.context': string;
'id': string,
'businessPhones': any[],
// ...
}
user-info.class.ts
import { IUserInfo, IUserInfoJson } from './user-info.interface.ts';
export class UserInfo implements IUserInfo {
// JSON specific properties
odataContext: string;
id: string;
businessPhoneNumbers: any[];
// ...
// other properties
fromJsonObj: boolean;
constructor(userInfoJson: IUserInfoJson) {
const isObj = userInfoJson && typeof userInfoJson === 'object' && userInfoJson.constructor === Object;
this.fromJsonObj = isObj;
// JSON mapping
this.odataContext = isObj ? userInfoJson['#odata.context'] : '';
this.id = uisObj ? serInfoJson['id'] : '';
this.businessPhoneNumbers = this.parseBusinessPhoneNumbers(userInfoJson);
// ...
}
parseBusinessPhoneNumbers(userInfoJson: IUserInfoJson): any[] {
return this.fromJsonObj && userInfoJson['businessPhones'] && userInfoJson['businessPhones'].length
? userInfoJson['businessPhones'].map(...)
: [];
}
}
And then
const userData = new UserInfo(<IUserInfoJson>response);
Following #jonrsharpe comment, I will add that if you interact with an API, I would suggest to follow the "Model-Adapter Pattern".
In essence, you create a Bridge (Decouple an abstraction from its implementation so that the two can vary independently), and you use it to create any instance of the desired model in your application.
You can read more about the motivation here:
https://dev.to/florimondmanca/consuming-apis-in-angular--the-model-adapter-pattern-3fk5