I'm trying to see if I can get VS Code to assist me when typing an object with predefined types.
A dish object can look like this:
{
"id": "dish01",
"title": "SALMON CRUNCH",
"price": 120,
"ingredients": [
"MARINERAD LAX",
"PICKLADE GRÖNSAKER",
"KRISPIG VITLÖK",
"JORDÄRTSKOCKSCHIPS",
"CHILIMAJONNÄS"
],
"options": [
{
"option": "BAS",
"choices": ["RIS", "GRÖNKÅL", "MIX"],
"defaultOption": "MIX"
}
]
}
My current attempt looks like this
enum DishOptionBaseChoices {
Rice = 'RIS',
Kale = 'GRÖNKÅL',
Mix = 'MIX',
}
type DishOption<T> = {
option: string
choices: T[]
defaultOption: T
}
type DishOptions = {
options: DishOption<DishOptionBaseChoices>[]
}
type Dish = {
id: string
title: string
price: number
ingredients: string[]
options: DishOptions
}
(not sure if some of them should be interface instead of type)
the plan is to make enums for all "types" of options. The auto-suggestion works fine with the id but not when writing the options.
Working solution
type ChoiceForBase = 'RIS' | 'GRÖNKÅL' | 'MIX'
type DishOption<OptionType, ChoiceType> = {
option: OptionType
choices: ChoiceType[]
defaultOption: ChoiceType
}
type Dish = {
id: string
title: string
price: number
ingredients: string[]
options: DishOption<'BAS', ChoiceForBase>[]
}
On type Dish you defined options: DishOptions, but then on type DishOptions you again defined a property options. So with you current types definition your options property looks like this:
const dish: Dish = {
options: {
options: [{
choices: ...
}]
}
}
If you want options to be an array of DishOption change your Dish definition:
type Dish = {
id: string
title: string
price: number
ingredients: string[]
options: DishOption<DishOptionBaseChoices>[]
}
Related
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 have types:
type TUser= {
id: number,
name: string,
}
type TFields = {
value: number,
name: string,
otherField: string,
}
type TTask = {
id: number,
title: string,
}
type TDataMethod = {
"TField": "fields",
"TTask": "tasks",
}
base on this types, how i can create type something like that (the part of the Type below is pseudocode):
type TResponse<T> = {
data: T extends TUser ? TUser[] : {[TDataMethod[T]]: T}
time: string,
}
for objects
const userResponse: TResponse<TUser> = {
data: [
id: 1,
name: "John",
],
time: "13 august 2022"
}
const taskResponse: TResponse<TTask> = {
data: {
tasks: {
id: 1,
title: "Some task",
}
},
time: "14 august 2022"
}
or i have one way - use extends declaration?
It is possible with some Typescript "programming".
For example, I have these interfaces.
interface User {
name: string;
age: number;
}
interface Bot {
name: string;
author: string;
}
The Metadata should be an array so we could iterate from it.
type Metadata = [
{
name: 'users'; // here's the property
type: User; // here's the type
},
{
name: 'bots';
type: Bot;
}
];
We don't actually could iterate from it. So, create an helper named ArrayUnshift which will unshift (remove first item) from the generic type. If the generic type (Array) is [first, ...rest], then return the rest so the first item is removed.
type ArrayUnshift<Array extends any[]> =
Array extends [infer First, ...infer Rest] ?
Rest : never;
Then we could itearate the Metadata. If the first Metadata.type is equal to generic type, then return the Metadata.name, if not recursive to itself but unshift the Metadata.
type MetadataProperty<T extends any, Data extends any[] = Metadata> =
Data[0]['type'] extends T ?
Data[0]['name'] : MetadataProperty<T, ArrayUnshift<Data>>;
Last, create ResponseData with MetadataProperty<T> as its property.
interface ResponseData<T extends object> {
time: string;
data: MetadataProperty<T> extends string ? {
[Key in MetadataProperty<T>]: T;
} : {
string: T; // fallback to avoid error
}
}
There's a repo that related to this topic, take a look to Type Challenges.
EDIT: Or you could simply use Extract utility as being said by #caTS.
You don't need to "iterate" over them; just get the elements as a union and use Extract: Extract<Metadata[number], { type: T }>["name"].
I have a json tree structure that I want to normalize into something like a hashmap and then denormalize it back to a tree if needed.
I have a very dynamic tree that I want to use as state in my react-redux project, but for that I somehow need to transform the data so that I can access it without having to search elements recursively in the tree each time I want to update/access the state.
const actual = {
id: "1",
type: 'Container',
data: {},
items: [
{
id: "2",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
},
{
id: "3",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
}
]
};
Now I want to transform it into something like:
const expected = {
1: {
id: "1",
type: 'Container',
data: {},
items: [1, 2]
},
2: {
id: "2",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
},
3: {
id: "3",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
}
};
I found a JS lib called Normalizr, but I absolutely don't get how to create the schemas for it to work.
That was my last try, and it returns only the inner two items and also directly the data object inside without id, items around:
const data = new schema.Entity("data");
const item = new schema.Object({ data });
item.define({ items: new schema.Array(item) });
const items = new schema.Array(item);
const normalizedData = normalize(mock, items);
I'm not going to worry too much about the types, since you can alter those to meet your needs. Going off you're example, I will define
interface Tree {
id: string;
type: string;
data: {
title?: string;
text?: string;
items: Tree[];
}
}
interface NormalizedTree {
[k: string]: {
id: string;
type: string;
data: {
title?: string;
text?: string;
items: string[]
}
}
}
and we want to implement function normalize(tree: Tree): NormalizedTree and function denormalize(norm: NormalizedTree): Tree.
The normalize() function is fairly straightforward since you can recursively walk the tree and collect the normalized trees into one big normalized tree:
function normalize(tree: Tree): NormalizedTree {
return Object.assign({
[tree.id]: {
...tree,
data: {
...tree.data,
items: tree.data.items.map(v => v.id)
}
},
}, ...tree.data.items.map(normalize));
}
In English, we are making a single normalized tree with a property with key tree.id and a value that's the same as tree except the data.items property is mapped to just the ids. And then we are mapping each element of data.items with normalize to get a list of normalized trees that we spread into that normalized tree via the Object.assign() method. Let's make sure it works:
const normalizedMock = normalize(mock);
console.log(normalizedMock);
/* {
"1": {
"id": "1",
"type": "Container",
"data": {
"items": [
"2",
"3"
]
}
},
"2": {
"id": "2",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
},
"3": {
"id": "3",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
}
} */
Looks good.
The denormalize() function is a little trickier, because we need to trust that the normalized tree is valid and actually represents a tree with a single root and no cycles. And we need to find and return that root. Here's one approach:
function denormalize(norm: NormalizedTree): Tree {
// make Trees with no children
const treeHash: Record<string, Tree> =
Object.fromEntries(Object.entries(norm).
map(([k, v]) => [k, { ...v, data: { ...v.data, items: [] } }])
);
// keep track of trees with no parents
const parentlessTrees =
Object.fromEntries(Object.entries(norm).map(([k, v]) => [k, true]));
Object.values(norm).forEach(v => {
// hook up children
treeHash[v.id].data.items = v.data.items.map(k => treeHash[k]);
// trees that are children do have parents, remove from parentlessTrees
v.data.items.forEach(k => delete parentlessTrees[k]);
})
const parentlessTreeIds = Object.keys(parentlessTrees);
if (parentlessTreeIds.length !== 1)
throw new Error("uh oh, there are " +
parentlessTreeIds.length +
" parentless trees, but there should be exactly 1");
return treeHash[parentlessTreeIds[0]];
}
In English... first we copy the normalized tree into a new treeHash object where all the data.items are empty. This will eventually hold our denormalized trees, but right now there are no children.
Then, in order to help us find the root, we make a set of all the ids of the trees, from which we will remove any ids corresponding to trees with parents. When we're all done, there should hopefully be a single id left, that of the root.
Then we start populating the children of treeHash's properties, by mapping the corresponding data.items array from the normalized tree to an array of properties of treeHash. And we remove all of these child ids from parentlessTreeIds.
Finally, we should have exactly one property in parentlessTreeIds. If not, we have some kind of forest, or cycle, and we throw an error. But assuming we do have a single parentless tree, we return it.
Let's test it out:
const reconsitutedMock = denormalize(normalizedMock);
console.log(reconsitutedMock);
/* {
"id": "1",
"type": "Container",
"data": {
"items": [
{
"id": "2",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
},
{
"id": "3",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
}
]
}
} */
Also looks good.
Playground link to code
I would recommend .flatMap for this kind of transformations:
const flattenTree = element => [
element,
...element.data.items.flatMap(normalizeTree)
]
This move you from this shape:
{
id: 1,
data: { items: [
{
id: 2,
data: { items: [
{ id: 3, data: { items: [] } },
] }
] }
}
to this one:
[
{ id: 1, data: {...}},
{ id: 2, data: {...}},
{ id: 3, data: {...}},
]
Then once you have a flat array, you can transform it further to remove the references and create an object from entries:
const normalizedTree = element => {
let flat = flattenTree(element)
// only keep the id of each items:
// [{ id: 1, data:{...}}] -> [1]
// for (const el of flat) {
// el.data.items = el.data.items.map(child => child.id)
// }
// note that the for loop will change the initial data
// to preserve it you can achieve the same result with
// a map that will copy every elements:
const noRefs = flat.map(el => ({
...el,
data: {
...el.data,
items: el.data.items.map(child => child.id),
},
}))
// then if you need an object, 2 steps, get entries, [id, elem]:
const entries = noRefs.map(({ id, ...element }) => [id, element])
// then the built-in `Object.fromEntries` do all the work for you
// using the first part of the entry as key and last as value:
return Object.fromEntries(entries)
// if you have multiple objects with the same id's, only the last one
// will be in your returned object
}
I'd like to create some constants that define a hierarchy of categories with subcategories. These will then be re-used to render different pages in an application, but by defining them as constants in one place I can quickly scaffold pages.
For example, given categories like:
const CATEGORIES = [
{
id: 'diet',
name: 'Diet',
color: 'green',
},
{
id: 'energy',
name: 'Energy',
color: 'yellow',
},
...
]
But now I'd like to have these objects be typed with TypeScript. I could use the as const syntax to infer a category type of:
{
id: 'diet' | 'energy' | ...
name: 'Diet' | 'Energy' | ...
color: 'green' | 'yellow' | ...
}
Which is great. (Although I don't really need the name/color specificity.)
But then when defining subcategories I run into a problem...
const SUBCATEGORIES = [
{
id: 'red_meat',
category: 'diet',
name: 'Red Meat',
emoji: '🥩',
},
...
]
If I use as const on the SUBCATEGORIES object then there's no type safety for the category key. I could easily misspell 'deit' and never know.
But if I use a declaration like:
const SUBCATEGORIES: Array<{
id: string
category: typeof CATEGORIES[number]['id']
...
}> = [
...
]
Then I can no longer infer the id key strings from subcategories later, because they will have a type of string instead.
Is there a way to define these sorts of metadata structures in TypeScript to have some a middle-ground between as const and defined types? How can I get the id keys to be inferred as constants while the rest is typed?
I'd be open to defining them as objects instead of arrays, but I can't seem to figure out a way that that helps.
In cases like this I usually make a helper function which verifies that a value is assignable to a type without widening it to that type. Assuming CATEGORIES looks like
const CATEGORIES = [
{
id: 'diet',
name: 'Diet',
color: 'green',
},
{
id: 'energy',
name: 'Energy',
color: 'yellow',
},
// ...
] as const;
then the helper function asSubcategories could look like this:
const asSubcategories = <S extends ReadonlyArray<{
id: string;
category: typeof CATEGORIES[number]['id'];
name: string;
emoji: string;
}>>(s: S) => s;
Here asSubcategories literally just returns its input without changing it, but it only accepts arguments of generic type S that is assignable to the type you care about. Then you call it like this:
const SUBCATEGORIES = asSubcategories([
{
id: 'red_meat',
category: 'diet',
name: 'Red Meat',
emoji: '🥩',
},
//...
] as const);
Since there's no error, you didn't misspell the category, and the type of SUBCATEGORIES is still
/* const SUBCATEGORIES: readonly [{
readonly id: "red_meat";
readonly category: "diet";
readonly name: "Red Meat";
readonly emoji: "🥩";
}] */
If you do misspell things, you should get a big error:
const BADSUBCATEGORIES = asSubcategories([
{
id: 'read_met',
category: 'deit',
name: 'Read Met',
emoji: '🍔',
},
//...
] as const); // error!!
/* Types of property 'category' are incompatible.
Type '"deit"' is not assignable to type '"diet" | "energy"'
*/
Yay! There are ways to use helper functions to narrow some properties while widening others, but the above should hopefully be enough to help you proceed. Good luck!
Playground link to code
I should create a js class respecting a json format
{
"rows": [{
"value": {
"comments": ${dInfo.description},
"Tags": [],
"metadataList": {
"names": [],
"metadata": {}
},
}]
}
I would like to know how to declare metadata.
I'm declaring like this actually.
export class Value {
comments: string;
Tags:string[];
metadataList:{
name:string[],
metadata: Object
}
}
Here metadataList is not typed which is not a best practice I think.
how could I declare metadataList with it's type ?
If metadata is a key/value store, you can make the type a little narrower than Object or {} by tightening up the value types:
class Value {
comments: string;
Tags:string[];
metadataList:{
name:string[],
metadata: { [key: string]: string }
}
}
const val = new Value();
val.metadataList = {
name: ['Example'],
metadata: {
key: 'value',
key2: 'value'
}
};
If you have more information about your metadata, I may be able to make it even narrower.