Restrict string value input from Node Express request query? - javascript

export type TodoQuery = {
sortBy?: 'createdAt' | undefined;
}
export const extractTodoQuery = (reqQuery: ParsedQs): TodoQuery => {
return {
sortBy: reqQuery.sortBy as 'createdAt' | undefined,
};
}
The reqQuery from extractTodoQuery function above comes from req.query in a node express app. I am trying to limit the sortBy property to certain string values or it should be undefined. Currently sortBy can still have different values in the return object based on the request query parameter. What would be an elegant way to achieve this in typescript?

You likely will want some kind of helper method to validate and return a strongly typed value. The cast doesn't force any conversion on its own and requires some validation logic.
There are many ways you could achieve this, but here is one approach by first defining an array of accepted values. Then, a helper that verifies sortBy is a string and is one of the values. If it's not a string or not one of the valid values, then it filters it out and returns undefined.
interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
// Valid sorting options. Change this as you please.
// The `as const` assertion ensures only these values are valid (not any `string`).
const SORT_BY = ['createdAt', 'updatedAt', 'name'] as const;
export type TodoQuery = {
// Infer the type based on the array so as you add/remove values the
// types automatically update.
sortBy?: typeof SORT_BY[number] | undefined;
}
// Helper to do the actual validation and return a strongly typed value.
const parseSortBy = (res: ParsedQs) => {
if (typeof res.sortBy === "string") {
return SORT_BY.find(valid => valid === res.sortBy)
}
return undefined;
}
export const extractTodoQuery = (reqQuery: ParsedQs): TodoQuery => {
return {
sortBy: parseSortBy(reqQuery),
};
}
TypeScript playground link.

Related

How to declare in Typescript a dictionary of Generic (in the TS sense) functions

I want to create a series of functions of a Generic type called Handler. Each function gets different but quite specific parameters and returns specific responses, all stated in the Generic definition. A message containing a request with RequestParams comes in and a reply goes back.
A dictionary of such functions is passed to a dispatcher function that will, when called with the handler name and the arguments, to return whatever that function does. To make it simple I declared functions for CRUD operations (plus list) just because everyone knows those.
But my problem is that I have not been able to get rid of the last error, shown in the comment at the end. I tried several ways to do it and been playing whack-a-mole fixing one function and getting another popping up.
I believe I declared too much and made it too restrictive or didn't state the options right. What am I missing? Thanks for your patience.
type ID = string;
type VALUE = string | number | boolean | Date;
type AnyRow = Record<string, VALUE>;
// Default types for Id, In and Out
type DefaultId = ID | undefined;
type DefaultIn = AnyRow | undefined;
// Listings always return an array, even if an empty one, never undefined.
// Reads might return a row or undefined. Tried with `null` as well, to no avail.
type DefaultOut = AnyRow[] | (AnyRow | undefined);
// A handler might receive an Id or not, and it might accept (In) and return (Out) various responses
// - the return is actually a Promise.
// - There is actually a third input, a series of options,
// such as, sort order, field to sort by, page to select
export type Handler<
Id extends DefaultId,
In extends DefaultIn,
Out extends DefaultOut
> = (params: { id: Id; data: In }) => Out;
export type Handlers = Record<
string,
Handler<DefaultId, DefaultIn, DefaultOut>
>;
// Given a series of handlers, `createDispatcher` will return
// a function that when called with the key to a handler and
// an object with the id and data
// returns whatever that keyed function returns, if anything.
export const createDispatcher =
(handlers: Handlers) =>
(fnName: string, requestData: { id: DefaultId; data: DefaultIn }) =>
handlers[fnName](requestData);
// These are the types for a typical CRUD series of handlers, plus list
// They represent the most common combinations of inputs and outputs.
type Resolvers<T extends AnyRow> = {
list: Handler<undefined, undefined, T[] | undefined>;
create: Handler<undefined, T, T>;
read: Handler<ID, undefined, T | undefined>;
update: Handler<ID, T, T>;
delete: Handler<ID, undefined, undefined>;
};
type Data = { a: number };
const handlers: Resolvers<Data> = {
list: () => [{ a: 1 }, { a: 2 }],
read: ({ id }) => (id ? { a: 1 } : { a: 2 }),
create: ({ data }) => data,
update: ({ id, data }) => (id ? data : data),
delete: ({ id }) => (id ? undefined : undefined),
};
// And here I create the actual dispatcher for the Data above
// with the text of the error shown below
export const dataDispatcher = createDispatcher(handlers);
// ^^^^^^^^
// Argument of type 'Resolvers<Data>' is not assignable to parameter of type 'Handlers'.
// Property 'list' is incompatible with index signature.
// Type 'Handler<undefined, undefined, Data[] | undefined>' is not assignable to type 'Handler<DefaultId, DefaultIn, DefaultOut>'.
// Type 'DefaultId' is not assignable to type 'undefined'.
// Type 'string' is not assignable to type 'undefined'.
Playground link
As #kelly pointed out in the comments, the solution is in changing this declaration:
export type Handlers = Record<
string,
Handler<DefaultId, DefaultIn, DefaultOut>
>;
to:
export type Handlers = Record<
string,
Handler<any, any, any>
>;
The issue was unrelated to the rest of the code shown above.
Sorry for the trouble and thanks.

method that takes parameter of multiple type and decide the behavior at runtime

I have a method like this
square(num: number | string): number {
//
}
In this method I want to check the parameter's data type and invoke the logic accordingly. How do to that in TS?
Edit:
Specific types in question
export type KV<T extends string | number> = {
key: string;
val: T;
}
export type LogicalKV<T extends string | number> = {
logicalOperator: LogicalOperator
filters?: Filter<T>[];
logicalFilters?: LogicalFilter<T>[];
}
Function
class Foo {
read(filter: KV<string | number> | LogicalKV <string | number>): Promise<void> {
// if filter is of KV do this, else do that
}
}
Types are stripped at runtime unfortunately. But you could go the good old way of typeof
if (typeof num === "string") {/*it's string here*/} else {/*it's number here*/}
Typescript will also understand that and coerce to an appropriate type in each branch.
In case of custom types, like yours, it's going to be a bit different. Algebraic data types are involved. It's also not too pretty when you instantiate it, but you have to deal with passing runtime string "kinds" one way or another.
export type KV = {
kvField: string;
kind: "kv";
};
export type LogicalKV = {
logicalKvField: number;
kind: "logicalKv";
};
const KVInstance: KV = {
kind: "kv",
kvField: "foo"
};
const LogicalKVInstance: LogicalKV = {
kind: "logicalKv",
logicalKvField: 1
};
type FilterArg = KV | LogicalKV;
const fn = (f: FilterArg) => {
switch (f.kind) {
case "kv":
// ts knows it's KV
f.kvField;
return;
case "logicalKv":
// ts knows it's LogicalKV
f.logicalKvField;
return;
}
}

Linter complaining can't use the spread operator on string, which is good, but how do I avoid it in this situation?

When I hover over ...res in VSCode I get the warning from my linter:
Spread types may only be created from object types
When I log res it's either a string or an object. However, I have no idea how to satisfy the linter in this case.
function getCookie(req: Request, key: string): string | undefined {
const {
headers: { cookie },
} = req;
return (
cookie &&
cookie.split(";").reduce<string | undefined>((res, item) => {
const data = item.trim().split("=");
return <string | undefined>{ ...res, [data[0]]: data[1] };
}, "")
);
}
So let's deal with the JavaScript and TypeScript aspects separately. In plain JavaScript terms, your function doesn't do what you've described. When there are cookies defined, it returns a Record<string, string> mapping cookie names to cookie values, not a particular string or false. If you mean for it to do something with key, you haven't included that part in your example.
When no cookies are defined, it ought to return undefined, but because you test the truthiness cookies in your return statement, it could return an empty string instead. That can be fixed by making that test separately and returning undefined explicitly. (Make sure your linter is warning you about using non-booleans in boolean context in TS.)
The reason you're spreading a string is that you're passing an empty string "" as the initial value to Array.prototype.reduce. Since you are accumulating objects, not strings, your initial value should be {} (an empty object) instead.
That helps us see the TypeScript issues. Your string | undefined casts are all unnecessary once the initial value is corrected, and the return type of the function overall becomes Record<string, string> | undefined. In sum:
function getCookie(req: Request, key: string): Record<string, string> | undefined {
const {
headers: { cookie },
} = req;
if (!cookie) {
return undefined;
}
return cookie.split(";").reduce<Record<string, string>>((res, item) => {
const data = item.trim().split("=");
return { ...res, [data[0]]: data[1] };
}, {});
}
However, if you actually want to look for a specific key and return its value, an array reducer isn't the best approach and your function would look rather different.

Convert Maybe<string>[]' to 'string[]

I am using graphql with generated types and struggling with how to convert them to the type I need to pass it to my data service calls.
#graphql-codegen has given me an args type of
export type QueryOrdersArgs = {
ids: Array<Maybe<Scalars['ID']>>;
};
(I don't really understand why its generated as a Maybe type, as the graphql schema enforces that I only query with a parameter of an array of ids (strings))
In my resolver, I need to call into a service which takes an array of strings. Everything works as expected (with #ts-ignore) but now I need to fix up my types.
const { orderIds } = args;
const result = getOrder(orderIds);
I have a codesandbox with just the types here https://codesandbox.io/s/typescript-playground-export-forked-3fpx9?file=/index.ts
export type Maybe<T> = T | null;
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
_FieldSet: any;
};
let ids: Array<Maybe<Scalars["ID"]>>;
export const getOrders = (orderIds: Array<string>) => {
orderIds.map((x) => console.log(x));
};
getOrders(ids);
I currently get the error - "TS2345: Argument of type 'Maybe[]' is not assignable to parameter of type 'string[]'."
Any help greatly appreciated
If you're confident that it shouldn't be a Maybe type, you can cast it:
type Maybe<T> = T | null;
const maybeArray: Maybe<string>[] = [];
let stringArray: string[] = maybeArray as string[];
or in your case
getOrders(ids as string[]);
To remove Maybe, you need to filter non nullable items
const nonNullable = <T>(value: T): value is NonNullable<T> =>
value !== null && value !== undefined
getOrders(ids.filter(nonNullable));
But if you want to remove the Maybe from your schema, you need to an exclamation mark ! in the graphql schema to be a required field

Specifying types with TypeScript, when <Array> has different kinds of structure

I am kind of new to TypeScript and I am trying to get rid of all any types.
Problem:
Within the React Component, I loop over an array of objects and extract a key/value pair.The components receives tags, tagKeys props as follows:
tags: [{ name: "Some tag" }] OR [{ platform: { name: "Some tag" } }];
tagKeys: ["name"] OR tagKeys: ["platform", "name"]
If I run this script (below), I get this error in the console:
Element implicitly has an 'any' type because index expression is not of type 'number'
type Props = {
tags: any[];
tagKeys: string[];
};
const Tags: React.FC<Props> = ({ tags, tagKeys }: PropsWithChildren<Props>) => {
const renderTags = (): React.ReactNode => {
return tags.map(tag => {
let key = "";
// Loop through tagKeys in the tags array to extract the tag
for (let i = 0; i < tagKeys.length; i++) {
key = i === 0 ? tag[tagKeys[i]] : key[tagKeys[i]];
}
return <Tag key={`tag-${key}`} tag={key} />;
});
};
return <StyledTags>{renderTags()}</StyledTags>;
};
If I change the line inside the for loop to...
key = i === 0 ? tag[tagKeys[i] as any] : key[tagKeys[i] as any];
...the script runs but I want to get rid of the any type.
Question:
Is there a way to set the type without specifying how the received props array tags will look?
I want to be able to pass arrays with different kinds of structure and extract a key/value pair.
There two issues to watch that we'll need to be clear about.
First, you asked:
Is there a way to set the type without specifying how the received props array tags will look?
type Props = {
tags: any[]; // This is the answer to your direct question
tagKeys: string[];
};
Secondly, having handled the above, TypeScript is actually complaining about your index.As you'll agree, reactjs uses the unique key to render each <Tag>.
// TypeScript is actually warning you about this.
// Note that initially, `key` is of type `string` (implicitly or indirectly )
let key = "";
// Within your loop, you assign `key` to be of type `object`
key = i === 0 ? tag[tagKeys[i]] : key[tagKeys[i]];
// So the above line says that `key` can be `string` or `object`
// Thus TypeScript tells you that...
// implicitly your index (key) expression is of type any
To handle that TypeScript error, update as follows:
let key: any = "";
Kindly make the necessary updates, and let me know how it goes for you.
key can be either a string or an object during it's lifetime, so my first intuition would be to declare it as such:
let key: object | string = "";
But TS is not happy with that, so my second try would be:
type NestedObject = { [key: string]: NestedObject } | string
let key: NestedObject = '';
key = { platform: { name: 'dd' } }
key = key.platform.name
But TS is not happy with the last line either, so at this time I would just give up:
let key: any = '';

Categories

Resources