I have the following Javascript object:
{
"dataMap":{
"2027":{
"userId":2027,
"code":"abcdef",
"title":"abcdef",
"questions":1
}
"2028":{
"userId":2028,
"code":"abcdef",
"title":"abcdef",
"questions":1
}
}
}
It contains another object dataMap and inside that are other ojbects. Can someone help me by telling me how I can create an interface for the dataMap object?
What I would like is to have an interface so I can enter:
var a = b.dataMap[2027].userId // okay and allowed
var a = b.dataMap[2027].xxxyId // gives a typescript error
You can use an interface with an index signature:
interface User {
userId: number;
code: string;
title: string;
questions: number;
}
interface DataMap {
[index: number]: User;
}
And for your specific example where the data map is again contained in an object:
interface DataMapContainer {
dataMap: DataMap;
}
var b: DataMapContainer = { "dataMap": { ... } };
b.dataMap[2027].userId; // okay
b.dataMap[2027].xxx; // error
See also: TypeScript interface for object with arbitrary numeric property names?
Related
I'm a bit new to TypeScript and wondering how can I access the raw field of a Class that extends an Interface?
e.g.
export interface APIGatewayProxyEventHeaders {
[name: string]: string | undefined;
}
getMap(headers: APIGatewayProxyEventHeaders): Map<string, string> {
const map: Map<string, string> = headers.getUnderlyingMap();
return map;
}
So I want some nice way to achieve what that imaginary headers.getUnderlyingMap() call aims to do?
I'm not sure what the type of [name: string]: string | undefined is?
Is it some special TypeScript construct? All I seem to be able to do is headers["xxx"].
Thanks!
UPDATE:
Thanks for the help, I was able to just do this to achieve what I wanted:
export interface APIGatewayProxyEventHeaders {
[name: string]: string | undefined;
}
export class InternalEvent {
constructor(
public reqHeaders: {}) {
}
static fromInternalEvent(headers: APIGatewayProxyEventHeaders): InternalEvent {
return new InternalEvent(headers);
}
}
You can treat this type [name: string]: string | undefined as an object (dictionary), where all keys are strings and values are either string or undefined.
Here you have several examples to understand it better:
interface Dictionary {
[name: string]: string | undefined
}
const x: Dictionary = {
name: 'John',
surname: 'Doe'
} // ok
const y: Dictionary = {
name: 'John',
1: 'hello'
} // ok, because JS make coersion under the hood, so you can acces x['1']
/**
* Indexed types are more generic, because you can use any key you want
*/
y['any key I want'] // valid, returns undefined
y['()=>{}'] // like I said, any key)
const z: Dictionary = {
name: 23
} // error, value can be either string or undefined, but not number
APIGatewayProxyEventHeaders is an object, like this:
{
key1: "somevalue",
key2: "someothervalue",
key3: undefined
}
That interface definition for the object means that you will need to test both the existence of a key, and the presence of a value. If both are true, then you have a string.
Like this:
// const obj: APIGatewayProxyEventHeaders
if (!!obj[key]) {
// obj[key] is a string
}
if (obj.hasOwnProperty(key)) {
// obj[key] is string | undefined
}
You can convert this object into an Map like this:
const map = new Map(Object.entries(obj))
Now you have a Map with the values in it. But there is not much advantage to this, because you don't have any additional type information - just a different data structure.
How to declare the entire MyCustomObject interface can fallback to an empty object?
interface MyCustomObject {
name: string,
age: number
}
Below case is safe, it has default property of name and age as fallback, but sometime if the obj is from other source like an api, it can be some other type like an empty {} or even an empty []
const obj: MyCustomObject = {
name: "",
age: 0
}
I tried this
interface MyCustomObject {
name: string,
age: number
} | {}
it doesn't work that way.
the most correct way is to create a union
type IUnionType = MyCustomObject | {};
other ways to handle this is to make each property optional
interface MyCustomObject {
name?: string,
age?: number
}
Or create an optional object property where a union is defined in the original interface.
interface MyCustomObject2 {
obj: MyCustomObject | {}
}
examples
I have one interface having some member variables
suppose my interface name is IUser and variable name is iUser.
I have to make it undefine but I m getting error that we cannot make iUser = undefine
so I did something like this.
let obj: any;
this.iUser = obj;
obj = undefined
and it works for me
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.
I find allready some posts on google where people solve this problem. but i cant reproduce the solutions on my project.
My Interface:
declare module PlatformInterface {
export interface Design {
primaryColor: string;
backgroundImage: string;
}
export interface Saga {
id: string;
name: string;
short_desc: string;
desc: string;
manga: Manga[];
anime: Anime[];
}
export interface Root {
id: string;
name: string;
design: Design[];
saga: Saga[];
}
}
My Model:
export class PlatformModel implements PlatformInterface.Root {
id: string;
name: string;
design = [];
saga = [];
constructor(obj?: any) {
this.id = obj.name.toLowerCase().replace(' ', '-');
this.name = obj.name;
this.design = obj.design;
this.saga = obj.saga;
}
}
My Service:
#Injectable()
export class PlatformService {
public list$: Observable<PlatformModel[]>;
private _platform: AngularFirestoreCollection<PlatformModel>;
constructor(db: AngularFirestore) {
this._platform = db.collection<PlatformModel>('platforms');
this.list$ = this._platform.valueChanges();
}
/** Get Platform by id */
get(id: string): Observable<PlatformModel> {
return this._platform.doc<PlatformModel>(id).valueChanges();
}
/** Add / Update Platform */
set(id: string, platforms: PlatformModel) {
return fromPromise(this._platform.doc(id).set(platforms));
}
/** Remove Platform */
remove(id: string) {
return fromPromise(this._platform.doc(id).delete());
}
}
My function in Component.ts
constructor(public _platformService: PlatformService) {
}
addPlatform(name: string) {
if (name !== '') {
const platform = new PlatformModel({
name: name,
design: [],
saga: []
});
this._platformService.set(platform.id, platform).subscribe();
}
}
The Angular Compiler dont Throw any error, But when i try to fire the addPlatform Function i get in Browser this error:
ERROR Error: Function DocumentReference.set() called with invalid data. Data must be an object, but it was: a custom PlatformModel object
The Errors Says that the Data must be an object, but it is allready an object or not? i mean i define in the service it with:
public list$: Observable<PlatformModel[]>;
[] Makes it to an object or not?
I've found some clarification here Firestore: Add Custom Object to db
while firebase could send the data inside your object to the database, when the data comss back it cannot instantiate it back into an instance of your class. Therefore classes are disallowed
my workaround for custom class was
this.db.collection(`${this.basePath}/`).doc(custom_class.$key)
.set(Object.assign({}, JSON.parse(JSON.stringify(custom_class))))
.then( ret => {
log.debug('file added', ret);
}).catch( err => {
log.error(err);
});
so I guess in your case it would be
/** Add / Update Platform */
set(id: string, platforms: PlatformModel) {
return fromPromise(this._platform.doc(id).set(Object.assign({},JSON.parse(JSON.stringify(platforms))));
}
For adding a Map into Firestore document you'll have to use:
Object.assign({}, JSON.parse(JSON.stringify(YOUR_MAP)))
I know some of typescript's advantages are enabling type-safe functions -
but is it possible to ensure my function can only get objects with specific keys, or in other words - objects of specific structure ?
I know of many elegant ways to test if a nested key exists, such as [this one][1] ,
and I can of course run a small check at the beginning of my function - but the reason I'm asking this is because my function will be used by many other programmers - and I want to ensure they can understand what input they should insert just from the function's signature.
Example:
function printName(user){
console.log(user.details.name); // I want to ensure details.name exist
}
and I would wish to have some feature like:
function (user : {details: {name : string}}){
//same ...
}
[1]: https://stackoverflow.com/questions/2631001/javascript-test-for-existence-of-nested-object-key#answer-4034468 "this one"
interface User {
details:{name: string}
}
function printName(user:User){
console.log(user.details.name); // I want to ensure details.name exist
}
This is exactly the feature you desire:
function printName(user: { [key: string]: any, details: { [key: string]: any, name: string }}) {
console.log(user.details.name)
}
Allow any properties and require details + name.
More legible and protected against unintentional changes:
// oftentimes you put the following interfaces in extra files:
interface Details {
readonly [key: string]: any // = allow other properties than "name"*
readonly name: string
}
interface User {
readonly [key: string]: any // = allow other properties than "details"*
readonly details: Details
}
// *) consider explicitly specifying them
function printName(user: User) {
console.log(user.details.name)
}
And use the following code if you know other developers may call your function from plain JavaScript code (not TypeScript code):
function printName(user: User) {
const name = ((user || <User>{}).details || <Details>{}).name
if(name == void 0) {
console.error("user name missing")
}
console.log(name)
}
Code on TypeScript Playground
Use keyof if you want to retrieve a value from a specific property, this will highlight the incorrect property name.
You can also use Object.keys to check if the property exists.
interface UserProps {
name: string;
age: number;
}
export class User {
constructor(private data: UserProps) {}
get(propName: keyof UserProps): string | number {
const dataKeys = Object.keys(this.data);
if (!dataKeys.includes(propName)) {
throw Error(`Property ${propName} does not exists on user object`);
}
return this.data[propName];
}
}
You can use interfaces in typescript
export interface Details{
name:string,
age: number
}
export interface User{
details : {
[key: string]: Details
};
}
function printName(user : User){}