I have just started TypeScript and I want to know how to declare an interface for this type of object:
const branch = {
'CN': {
'name': 'CN Name',
'branch': 'Chinoise',
'url': 'CN URL'
},
'DE': {
'name': 'DE Name',
'branch': 'Allemande',
'discord': 'DE Discord',
'url': 'DE URL'
},
'EN': {
'name': 'EN Name',
'branch': 'Anglaise',
'url': 'EN URL'
},
[...]
}
As you can see, I've got this interface:
interface Branch {
name: string,
branch: string,
discord?: string,
url: string
}
Repeated several times in the above code.
So I wanted to know if it was possible to say to TypeScript:"Hey, the Branch object contains this interface which is repeated many times".
Thanks !
You can do it like this:
const branch: {[key: string]: Branch} = ...;
Which means that branch variable is an object whose keys are of type string and values are of type Branch;
Official Docs for index signatures: https://www.typescriptlang.org/docs/handbook/interfaces.html
Related
I am getting type error on graphql argument field that's been defined as string. Strangely it expects ID type instead.
Argument field in question: createdBy
When I type in random ID that I get from for example mongodb it proceeds, thinking it's ID type.
Renaming my field to something else for example createdUsername it works as expected, receiving strings properly.
I wish to keep the name createdBy as it is string type and wondering what's causing the weird type, name collision issue. Any help is appreciated.
I am using koa, koa-graphql, and bare graphql on my backend.
Code for my input type definition:
const clientFilterInputFields = {
type: {type: GraphQLID},
company: { type: GraphQLString },
person: { type: GraphQLString },
phone: { type: GraphQLString },
email: { type: GraphQLString },
social: { type: GraphQLString },
createdBy: { type: GraphQLString },
createdAt: { type: dateInputType }
}
const clientFilterInputType = new GraphQLInputObjectType({
name: 'clientFilterInput',
fields: clientFilterInputFields
})
export const getClientsArgs = {
clientFilterInput: { type: new GraphQLNonNull(clientFilterInputType) },
pagingInput: { type: new GraphQLNonNull(pagingInputType) }
}
Explanation
I'm working with react-select and want to generate the options array which would be passed to the react-select component.
The options array is of the type:
type TOptions = { value: string; label: string }[]
I get the data for the options array from an API. The data will have a structure like:
{
name: string;
slug: string;
id: number;
}[]
So, I created a helper function to transform the data into the options array format.
const generateSelectOptions = (
data: Record<string, string>[],
field: { valueKey: string; labelKey: string }
) => {
const options = data.map((data) => ({
value: data[field.valueKey],
label: data[field.labelKey],
}));
// TODO: create a better type check
return options as TOptions;
};
This function will have get two params,
data - data received from the API
field - which is an object that would contain the keys, valueKey and labelKey from the data object that would be mapped appropriately to the options array.
The function I created works fine, but I have manually asserted the return type of the function as TOptions.
Example
const data = [
{
name: "Holy Holy Holy",
slug: "holy-holy-holy",
id: 1,
},
{
name: "Amazing Grace",
slug: "amazing-grace",
id: 2,
},
];
const options = generateSelectOptions(data, {
valueKey: "slug",
labelKey: "name",
});
// now options would be
options = [
{
label: "Holy Holy Holy",
value: "holy-holy-holy",
},
{
label: "Amazing Grace",
value: "amazing-grace",
},
];
Question
Now, I'm thinking of a better way to type generateSelectOptions function, where when calling the function, as soon as I give the first argument data, the fieldKeys object which would be the second argument should automatically get type inference, as the fieldKeys - valueKey and labelKey can only be of the type keyof data
Is there a way to achieve this?
I would really appreciate some help here. Thanks
Sure, use a generic type parameter which will be inferred from the first argument, the second argument will then be validated against it:
function getOptions<T>(
data: T[],
field: { valueKey: keyof T }
) { /*...*/ }
type TOptions = { value: string; label: string}[];
function generateSelectOptions<T extends Record<string, unknown>, KeyField extends keyof T, LabelField extends keyof T>(data: T[], field: {valueKey: KeyField, labelKey: LabelField}): TOptions {
return data.map(entry => ({
label: String(entry[field.labelKey]),
value: String(entry[field.valueKey])
}))
}
const data = [
{
name: "Holy Holy Holy",
slug: "holy-holy-holy",
id: 1,
},
{
name: "Amazing Grace",
slug: "amazing-grace",
id: 2,
},
];
const options: TOptions = generateSelectOptions(data, {valueKey: 'id', labelKey: 'name'});
Obviously, without casting to a string, the values of your data record contain arbitrary values (e.g. strings and numbers). If you only want the label to be a number, you need to cast everything you might find there to a string (e.g. with String(value).
You could use generics, create a type for your data and pass it into your generateSelectOptions function.
You would still need to cast values that you will get from data[field.valueKey] and data[field.labelKey] to a string to match TOptions type
This is what I came up so far and if you will try to replace valueKey and labelKey values to something else rather than slug or name or id your IDE will give you a type hint
type TOption = { value: string; label: string };
const generateSelectOptions = <T>(
data: T[],
field: { valueKey: keyof T; labelKey: keyof T }
): TOption[] => {
const options = data.map<TOption>((data) => ({
value: data[field.valueKey] as string,
label: data[field.labelKey] as string,
}));
return options;
};
type DataType = { name: string; slug: string; id: number };
const data: DataType[] = [
{
name: "Holy Holy Holy",
slug: "holy-holy-holy",
id: 1,
},
{
name: "Amazing Grace",
slug: "amazing-grace",
id: 2,
},
];
const options = generateSelectOptions<DataType>(data, {
valueKey: "slug",
labelKey: "name",
});
console.log(options);
you could even add some extra layer of validation on top of the options that gets generated using type guard, but I think that should not be neccessary
const isOptionGuard = (option: any): option is TOption => "value" in option && "label" in option;
const isValidType = options.every(isOptionGuard);
console.log(isValidType)
if (!isValidType) throw Error("GenerateSelectOptions failed");
I'm trying to call the Uniswap Router contract like:
// dummy data
Function: swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline)
MethodID: 0x7ff36ab5
[0]: 000000000000000000000000000000000000000000000000000000dd0f593444
[1]: 0000000000000000000000000000000000000000000000000000000000000080
[2]: 00000000000000000000000047c0a182235478ca13d248d049eaa28d4ff7520f
[3]: 000000000000000000000000000000000000000000000000000000005f9a1e44
[4]: 0000000000000000000000000000000000000000000000000000000000000002
[5]: 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
[6]: 0000000000000000000000001a969239e12f07281f8876d11afcee081d872adf
In my TypeScript code I have:
const abi = web3.eth.abi.encodeFunctionCall(
{
type: 'function',
name: 'swapExactTokensForTokens',
inputs: [
{
name: 'amountIn',
type: 'uint256',
},
{
name: 'amountOutMin',
type: 'uint256',
},
{
name: 'path',
type: 'address[]',
},
{
name: 'to',
type: 'address',
},
{
name: 'deadline',
type: 'uint256',
},
],
},
[
fromWei,
toWei,
// the problem
web3.eth.abi.encodeParameter('address[]', [
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
'0x20fe562d797a42dcb3399062ae9546cd06f63280',
]),
getAddress(),
deadline,
]
)
From the error, it looks like encodeFunctionCall wasn't expecting an encoded array (at least, the way I did it). So how would I pass an array of addresses?
(node:21008) UnhandledPromiseRejectionWarning:
Error: expected array value
(argument="path", value="0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000020fe562d797a42dcb3399062ae9546cd06f63280", code=INVALID_ARGUMENT, version=abi/5.0.0-beta.153)
Full source: https://github.com/ZaneH/tradr/blob/56d670aeec09ed0e8c3e359c48ae8b36fdea7c30/packages/backend/src/modules/Trades.ts#L37
I believe that web3.eth.abi.encodeFunctionCall expects JSON type parameters, e.g. address[] is represented as an simple array of strings ["0x...", "0x..."]
// the problem
web3.eth.abi.encodeParameter('address[]', [
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
'0x20fe562d797a42dcb3399062ae9546cd06f63280',
]),
Instead of Please try:
[
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0x20fe562d797a42dcb3399062ae9546cd06f63280"
],
I apologize in advance for the complex example here; I tried to trim it down as much as I could to illustrate what I try to achieve
I have a complex structure that I need to traverse and transform based on some conditions; Here's an (short) example of the structure that should cover most scenarios:
{ PROP1: {
metadata: Object, // somewhere deeper in metadata I have a `value` key
parent: { $ref: String },
employee: {
parent: { $ref: String },
id: String,
metadata: Object,
products: {
metadata: Object,
model: { $ref: String },
type: 'array',
value: ["String", "String" , "String"] ,
[...]
},
model: {
id: String,
email: {
metadata: Object,
value: 'a#b.com',
type: 'string',
validity: Object,
[...]
},
name: {
firstName: {
metadata: Object,
value: 'John',
type: String,
validity: Object,
[...]
},
lastName: {
metadata: Object,
value: 'Smith',
type: String,
validity: Object,
[...]
},
}
},
operations: {
id: String,
items: [
{ action: {value: "UPDATE", model: {$ref: String }, [...] },
{...}
],
metadata: Object,
[...]
}
}
},
PROP2: {
// similar as PROP1
},
[... and so on ...]
}
I basically need to clean that up before sending it to the backend;
Whenever a value contains $ref, I don't want the key/val pair (e.g.: PROP1.parent is of no use and can be omitted)
whenever a value contains value, I need to omit everything else and move the value of value as the value of key (e.g.: PROP1.employee.products should equal ['String', 'String', 'String'])
keys like id, metadata, validity (etc) can be completely omitted regardless of its content
So the end result should be along those lines:
{ PROP1: {
employee: {
products: ['item','item','item'],
model: {
email: 'a#b.com',
name: { firstName: 'John', lastName: 'Smith'},
},
operations: [
{action: 'UPDATE'}
]
}
},
PROP2: { ... }
}
I tried lots of different approaches using different lodash methods but couldn't wrap my head around this...
Any help will be greatly appreciated
Thanks
In pseudo code, try something like this. Implement the specifics and post more info when you run into trouble.
var ignoreKeyArray = ["id", ...] // keys to ignore and remove
var newJSON = "{}";
for (property in JSON) {
var newProp = parseJSON(key, property);
insert newProp in newJSON object
}
var parseJSON = function (key, jsonBlob) {
if (key in ignoreKeyArray || jsonBlob contains value "$ref")
return "";
var jsonOut = key + ": {}";
for (child in jsonBlob) {
add parseJSON(child) to jsonOut;
}
return jsonOut;
}
If you have any questions, comment so I can extend the answer and clarify.
I have Mongoose model instance:
var s = new Song({
song_id: '123',
title: 'sample song',
artist: {
name: 'sample artist',
id: '456'
}
});
Now I'd like to set/update its properties but using extend (e.g. from nodejs util._extend)
s = extend(s, {
title: 'changed title',
artist: {
name: 'changed artist',
id: '789'
}
});
s.save();
And while title (as a top-level property) gets set ok, changes in artist are not visible.
I know I can just set it via:
s.artist.name = 'changed artist';
but is there something I'm missing or is this way of updating properties not usable?
EDIT
Gah... Looks like someone defined schema the wrong way. Instead of artist field defined as
artist: {
name: String,
id: String
}
it was defined like
artist.name: String,
artist.id: String
When I redefined it it works now. Thanks
What you can do is to use Mongoose's update method:
Song.update({
song_id: '123',
}, {
$set: {
title: 'changed title',
artist: {
name: 'changed artist',
id: '789',
},
},
}, function(err, numAffected) {
...
});
However, I don't actually see why your extend attempt failed. It seems to work for me, I usually use underscore's _.extend function.