Parsing string literals into enum array - javascript

I'm receiving the following in an API response:
{ "roles": [ "ADMIN", "USER" ] }
where the response will always contain an array of roles (USER, PRESENTER, ORGANIZER, and ADMIN).
I want to convert it into a valid TypeScript array (Role[]), where the type Role is defined as follows:
export type Role = 'USER' | 'PRESENTER' | 'ORGANIZER' | 'ADMIN'
Any ideas?

Your Role type is not an enum. It is just a string type limited to certain values.
You can just cast the result as a Role[] and TypeScript will be happy. This assumes the incoming data never has a bad value!
const data: {roles: Role[]} = JSON.parse('{"roles": ["ADMIN", "USER"]}');
data.roles // TypeScript knows it is a Role[]

You can just cast it to your union type:
const apiRoleArray = ["ADMIN", "USER"];
const realRoleArray: Role[] = <Role[]>apiRoleArray;
BUT you probably want to validate its contents rather than just trusting the API. :-) Drawing on this question's answers, you can create the type by using the keys of an object rather than defining it literally (see the accepted answer there for why):
const roleStrings = {
USER: "",
PRESENTER: "",
ORGANIZER: "",
ADMIN: ""
};
export type Role = keyof typeof roleStrings;
then give yourself a validation function:
const isRole = (s: string): s is Role => {
return roleStrings.hasOwnProperty(s);
};
then a robust conversion function, for example:
const rawToRoleArray = (rawArray: string[]): Role[] => {
return rawArray.map(s => {
if (!isRole(s)) {
throw new Error("Invalid Role: " + s);
}
return <Role>s;
});
};
(you could combine those if you don't need them separately)
then use it:
// Valid
const realRoleArray: Role[] = rawToRoleArray(["ADMIN", "USER"]);
console.log(realRoleArray);
// Invalid
const realRoleArray2: Role[] = rawToRoleArray(["ADMIN", "FOO"]);
console.log(realRoleArray2);
Live in the playground | Live on jsFiddle

If I got you corectly thats what you want to do.
enum RoleEnum {
USER,
PRESENTER,
ORGANIZER,
ADMIN
}
const parseEnum = (name: String): RoleEnum => RoleEnum[`${name}`]
const parsed: RoleEnum[] = [ 'ADMIN', 'USER' ].map(parseEnum)
console.log(parsed)

Related

How to properly extract payload from nestjs/jwt token?

I ran into a problem with decrypting a token in a project, I want to extract data from the incoming token, compare it with the data from the database, and then perform certain actions.
The problem is that when I get the payload from "jwtService.decode()", I can't access the "decodedJwt.email" field, nest complains that "decodedJwt" is not an object. But if you return typeof "decodedJwt" then the answer is a string. However, I cannot access the "email" field in the code itself. If I return "decodedJwt" in this function, then in postman I will get the very necessary object with the necessary fields, including the same "email" field. What's my mistake?
Reply from nest:
Property 'email' does not exist on type 'string | { [key: string]: any; }'.
Property "email" does not exist on type "string".ts(2339)
async refreshTokenByOldToken(authHeader: string) {
const decodedJwt = this.jwtService.decode(authHeader.split(' ')[1])
return decodedJwt.email
}
You need to cast the type. Tell explicitly what type it is.
type PayloadType = {
email: string;
}
async refreshTokenByOldToken(authHeader: string) {
const decodedJwt = this.jwtService.decode(authHeader.split(' ')[1]) as PayloadType;
return decodedJwt.email
}
You can reference PayloadType other places as well for example in your service instead of string | { [key: string]: any; } you can have string | PayloadType.
examples of type casting
because your decodedJwt string is different data type. You access email information two cases;
If you #nestjs/jwt package
const decoded: JwtPayload = this.jwtService.decode(yourAccessToken);
const email = decoded.email;
OR
Base 64 Converting
const base64Payload = authHeader.split(" ")[1];
const payloadBuffer = Buffer.from(base64Payload, "base64");
const updatedJwtPayload: JwtPayload = JSON.parse(payloadBuffer.toString()) as JwtPayload;
const email = updatedJwtPayload.email;

Correctly sanitize data for request - Typescript, React

Background
I'm not sure how I should approach sanitizing data I get from a Java backend for usage in a React form. And also the other way around: sanitizing data I get from a form when making a backend request. For frontend/backend communication we use OpenApi that generates Typescript interfaces and API for us from DTOs defined in Java.
Scenario
Example of the Schema in Java:
public enum Pet {
CAT,
DOG
}
#Schema(description = "Read, create or update an account")
public class AccountDto {
#NotNull
private Boolean active;
#NotBlank
private String userName;
#NotNull
private Pet preferedPet;
#Nullable
private String bioDescription;
// Constructor and getter/setters skipped
}
Current implementation
Example of the generated Typescript interface:
enum Pet {
CAT,
DOG
}
interface AccountDto {
active: boolean,
userName: string,
preferedPet: Pet,
bioDescription?: string // Translates to: string | undefined
}
Example React.js:
import {getAccount, updateAccount, Pet, AccountDto} from "./api"
export default function UpdateAccount() {
const [formData, setFormData] = useState<AccountDto>({
active: true,
userName: "",
preferedPet: Pet.CAT,
bioDescription: ""
})
useEffect(() => {
async function fetchAccount() {
const response = await getAccount();
// Omitted error handling
setFormData(response.data);
// response.data could look like this:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.DOG,
// bioDescription: null
// }
}
}, [])
async function updateAccountHandler() {
const response = await updateAccount(formData);
// Omitted error handling
// Example formData object:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.CAT,
// bioDescription: ""
// }
}
return (
// All input fields
)
}
Problems
When fetching the account, bioDescription is null. React will throw a warning that a component (bioDescription input) is changing from uncontrolled to controlled.
If by any chance there is a situation where null is set for preferedPet we will get a warning that the select value is not valid.
When updating the account all empty strings should be null. Required for the database and generally cleaner in my opinion.
Questions
1.) I'm wondering how other React users prepare/sanitize their data for usage and requests. Is there a go to or good practice I'm not aware of?
2.) Currently I'm using the following function to sanitize my data. It seems to work and Typescript does not notify me about any type mismatches but I think it should since bioDescription can only be string | undefined and not null.
function sanitizeData<T>(data: T, type: "use" | "request"): T {
const sanitizedData = Object.create({});
for (const [key, value] of Object.entries(data)) {
if (!value && type === "use") {
sanitizedData[key] = "";
} else if (!value && type === "request") {
sanitizedData[key] = null;
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
}
I have a situation where I'm trying to manually change a prop without using the React setState.
formData.description = null;
At this point Typescript is telling me that null is not possible. That's how I detected that my sanitizer function might not be correct.
Demo
Sandbox - https://codesandbox.io/s/async-cdn-7nd2m?file=/src/App.tsx

Type assignment in Typescript

I don't understand why, in Typescript, I have this error in a varibale assignement; I think types are compatible, isn't it?
To work, I have to add:
async () => {
dataFiles = <Array<DataFileLoadingInstructionType>>await Promise.all(
but why?
The error:
Type
'(DataFile | { file_path: string; header_line: number; sheets: never[]; type: string; })[]'
is not assignable to type
'({ file_path: string; } & { file_info?: string | undefined; file_name_suffix?: string | undefined; command_parameters?: string[] | undefined; } & { type: "unknown"; })[]'.
This is my the index.ts example:
import { DataFileLoadingInstructionType } from "./types_async";
import * as path from "path";
type DataFile = {
file_path: string;
type: "unknown";
};
const originalDataFiles: Array<DataFile> = [];
originalDataFiles.push({ file_path: "myFile.txt", type: "unknown" });
let dataFiles: Array<DataFileLoadingInstructionType>;
async function convertPathIfLocal(dataFile: string) {
if (dataFile.indexOf("://") === -1 && !path.isAbsolute(dataFile)) {
dataFile = path.join("my_dir/", dataFile);
}
return dataFile;
}
(async () => {
//here, I have to add <Array<DataFileLoadingInstructionType>> to work
dataFiles = await Promise.all(
originalDataFiles
.filter((f) => f !== undefined)
.map(async (dataFile) => {
if (typeof dataFile === "string") {
return {
file_path: await convertPathIfLocal(dataFile),
header_line: 0,
sheets: [],
type: "csv",
};
} else {
dataFile.file_path = await convertPathIfLocal(dataFile.file_path);
return dataFile;
}
})
);
console.log(
`OUTPUT: ${JSON.stringify(
dataFiles
)} - type of dataFiles: ${typeof dataFiles}`
);
})();
And this is my the types.ts example:
import {
Array,
Literal,
Number,
Partial as RTPartial,
Record,
Static,
String,
Union,
} from "runtypes";
const UnknownDataFileLoadingInstructionTypeOptions = Record({
type: Literal("unknown"),
});
export const DataFileLoadingInstructionType = Record({ file_path: String })
.And(
RTPartial({
file_info: String,
file_name_suffix: String,
command_parameters: Array(String),
})
)
.And(Union(UnknownDataFileLoadingInstructionTypeOptions));
export type DataFileLoadingInstructionType = Static<
typeof DataFileLoadingInstructionType
>;
From what I read of these code snippets, types are actually not assignable.
On one hand, dataFiles is declared with the type Array<DataFileLoadingInstructionType>, in other words:
declare const dataFiles: Array<
| { file_path: string, file_info?: string, file_name_suffix?: string, command_parameters?: string[] }
| { type: 'unknown' }
>
On the other hand, the returned value of originalDataFiles.filter(...).map(...) is either:
{
file_path: string // await convertPathIfLocal(dataFile)
header_line: number // 0
sheets: never[] // [] inferred as Array<never>
type: string // "csv"
}
(cf. the returned object from the if branch, inside the map)
Or:
DataFile
(cf. the returned object from the else branch, inside the map)
So, we end up with:
dataFiles type:
Array<
| { file_path: string, file_info?: string, file_name_suffix?: string, command_parameters?: string[]}
| { type: 'unknown' }
>
await Promise.all(originalDataFiles.filter(...).map(...)) type:
Array<
| { file_path: string, header_line: number, sheets: never[], type: string }
| DataFile
>
And they are both, indeed, not assignable:
The properties header_line, sheets and type are missing on the type DataFileLoadingInstructionType
The property file_path is present in DataFile but not in UnknownDataFileLoadingInstructionTypeOptions
I'd say you should:
Add the 3 properties to the DataFileLoadingInstructionType, otherwise adapt the returned object in theif branch of the map to make it compatible with DataFileLoadingInstructionType.
Remove the property file_path from the dataFile returned in the else branch of the map, or add the file_path property to the UnknownDataFileLoadingInstructionTypeOptions type.
The essence of the problem lies in misunderstanding string literals, the limitations of type inferencing and duck typing. That is quite a lot, but I will try to explain it bit by bit.
Duck Typing
"If it walks like a duck and it quacks like a duck, then it must be a duck."
One of the nice things in Typescript that you do not need to instantiate a class in order for it to adhere to an interface.
interface Bird {
featherCount: number
}
class Duck implements Bird {
featherCount: number;
constructor(featherInHundreds: number){
this.featherCount = featherInHundreds * 100;
}
}
function AddFeathers(d1: Bird, d2: Bird) {
return d1.featherCount + d2.featherCount;
}
// The objects just need to have the same structure as the
// expected object. There is no need to have
AddFeathers(new Duck(2), {featherCount: 200});
This creates a lot of flexibility in the language. Flexibility you are using in the map function. You either create an entirely new object where you adjust some things or you adjust the existing dataFile. In this case that might easily be solved by creating a constructor or method that returns a new class. If there are a lot of transformations this might lead to very large classes.
Type Inferencing
However this flexibility comes at a cost in certain cases. Typescript needs to be able to infer the type, but in this case that goes wrong. When you create the new object, the type property is perceived as a string not the template literal "unknown". This exposes two problems in your code.
The type property can only contain a single value "unknown" because it is typed as a string literal with just one value as opposed to a union of multiple literals. The type should at least have the type of "unknown" | "csv" for this value to work. However I expect that this is just a problem in this example since adding the <Array<DataFileLoadingInstructionType>> seems to solve the problem for you, while in this example it would break the example.
But even if you would adjust this or pass the only allowed value "unknown" here it would still complain. This is how Typescript infers types, since you just assign a value here, it presumes that it is the more generic string as opposed to the more narrow literal "csv".
Solution
The trick is helping Typescript type the object you are creating. My suggestion would be to assert the type of the property type, so that Typescript knows that the string assignment is actually a string literal.
Example
import {
Literal,
Number,
Partial as RTPartial,
Record,
Static,
String,
Union,
} from "runtypes";
import * as path from "path";
// Create separate type, so that we don't need to assert the type inline.
type FileTypes = "unknown" | "csv"
const UnknownDataFileLoadingInstructionTypeOptions = Record({
type: Literal<FileTypes>("unknown"),
});
export const DataFileLoadingInstructionType = Record({ file_path: String })
.And(
RTPartial({
file_info: String,
file_name_suffix: String,
// Threw an error, commented it out for now.
// command_parameters: Array(String),
})
)
.And(Union(UnknownDataFileLoadingInstructionTypeOptions));
export type DataFileLoadingInstructionType = Static<
typeof DataFileLoadingInstructionType
>;
type DataFile = {
file_path: string;
type: FileTypes;
};
const originalDataFiles: Array<DataFile> = [];
originalDataFiles.push({ file_path: "myFile.txt", type: "unknown" });
let dataFiles: Array<DataFileLoadingInstructionType>;
async function convertPathIfLocal(dataFile: string) {
if (dataFile.indexOf("://") === -1 && !path.isAbsolute(dataFile)) {
dataFile = path.join("my_dir/", dataFile);
}
return dataFile;
}
(async () => {
//here, I have to add <Array<DataFileLoadingInstructionType>> to work
dataFiles = await Promise.all(
originalDataFiles
.filter((f) => f !== undefined)
.map(async (dataFile) => {
if (typeof dataFile === "string") {
return {
file_path: await convertPathIfLocal(dataFile),
header_line: 0,
sheets: [],
type: "csv" as FileTypes,
};
} else {
dataFile.file_path = await convertPathIfLocal(dataFile.file_path);
return dataFile;
}
})
);
console.log(
`OUTPUT: ${JSON.stringify(
dataFiles
)} - type of dataFiles: ${typeof dataFiles}`
);
})();

Typescrit interface for an object of arrow functions

I'v faced a problem trying to define an interface for the following structure:
interface JSONRecord {
[propName: string]: any;
}
type ReturnType = (id: string|number, field: string, record: JSONRecord) => string
export const formatDictionary = ({
mode = "render", key = "originalValue",
defaultKey = "originalValue"
}):ReturnType => (id, field, record) => {
...
}
interface Lookup {
Dictionary: ({mode, key, defaultKey}:{mode: string, key: string, defaultKey: string}) => ReturnType,
...
}
export const functionLookup:Lookup = {
Dictionary: formatDictionary,
...
}
export const formatField = (params:JSONRecord):string|ReturnType => {
const type:string = params.type
if (type === undefined) { return identity }
const fn = functionLookup[type]
if (fn === undefined) { return identity }
return fn({ ...params })
}
I'm getting the following errors:
In line const fn = functionLookup[type] : Element implicitly has an 'any' type becasue expression of type string can't be used to index type 'Lookup'. No index signature with parameter of type 'string' was found on type 'Lookup'.
I'm not really sure why is this happening, i thought that the Dictionary i defined in Lookup is supposed to be interpreted as a string. When i change Dictionary to [x: string]: ({mode, key, defaultKey}:{mode: string, key: string, defaultKey: string}) => ReturnType the error disappears, but i want to be specific with the arguments that can be passed.
In line return fn({ ...params }) : Expected 3 arguments, but got 1
I can't really wrap my head around this, the function is clearly expecting only 1 object as an argument {mode, key, defaultKey} or is it expecting the ReturnType function there?
I would appreciate any help. Thanks a lot in advance :)
In you case (from sandbox):
const anExampleVariable = "Hello World"
console.log(anExampleVariable)
// To learn more about the language, click above in "Examples" or "What's New".
// Otherwise, get started by removing these comments and the world is your playground.
interface Lookup {
test: number
}
const functionLookup:Lookup = {
test: 5
}
const params = {
type: 'test'
};
const type = params.type
const a = functionLookup[type]
params variable is infered as {type: string}.
Here functionLookup[type] you want use type as index for functionLookup, but TS does not work that way. Because you can't just use general type string as index for Lookup type.
Lookup allows you to use only literal test as index.
So you can add as const prefix to your params vvariable.
const params = {
type: 'test'
} as const;
You can make Lookup indexed:
interface Lookup {
test: number,
[prop:string]:number
}
Or, you can explicitly define a Record type for params:
const params:Record<string, keyof Lookup> = {
type: 'test'
}

Typescript and Mongoose: Property 'x' does not exist on type 'Document'

This is my Mongoose model that I use together with TypeScript:
import mongoose, { Schema } from "mongoose";
const userSchema: Schema = new Schema(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
name: {
type: String,
maxlength: 50,
},
...
...
}
);
userSchema.method({
transform() {
const transformed = {};
const fields = ["id", "name", "email", "createdAt", "role"];
fields.forEach((field) => {
transformed[field] = this[field];
});
return transformed;
},
});
userSchema.statics = {
roles,
checkDuplicateEmailError(err: any) {
if (err.code === 11000) {
var error = new Error("Email already taken");
return error;
}
return err;
},
};
export default mongoose.model("User", userSchema);
I use this model in my controller:
import { Request, Response, NextFunction } from "express";
import User from "../models/user.model";
import httpStatus from "http-status";
export const register = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = new User(req.body);
const savedUser = await user.save();
res.status(httpStatus.CREATED);
res.send(savedUser.transform());
} catch (error) {
return next(User.checkDuplicateEmailError(error));
}
};
I get the following errors:
Property 'transform' does not exist on type 'Document'.
Property 'checkDuplicateEmailError' does not exist on type 'Model<Document, {}>'.
I tried export default mongoose.model<any>("User", userSchema); and I do not get the transform error but still the error for checkDuplicateEmailError.
You know that mongoose.model("User", userSchema); creates a Model, but the question is: a model of what?
Without any type annotations, the model User gets the type Model<Document, {}> and the user object created from new User() gets the type Document. So of course you are going to get errors like "Property 'transform' does not exist on type 'Document'."
When you added your <any> variable, the type for user became any. Which actually gives us less information than knowing that user is a Document.
What we want to do is create a model for specific type of Document describing our user. Instances of the user should have a method transform() while the model itself should have the method checkDuplicateEmailError(). We do this by passing generics to the mongoose.model() function:
export default mongoose.model<UserDocument, UserModel>("User", userSchema);
The hard part is figuring out those two types. Frustratingly, mongoose doesn't automatically apply the fields from your schema as properties of the type, though there are packages that do this. So we have to write them out as typescript types.
interface UserDocument extends Document {
id: number;
name: string;
email: string;
createdAt: number;
role: string;
transform(): Transformed;
}
Our transform function returns a object with 5 specific properties from the UserDocument. In order to access the names of those properties without having to type them again, I moved the fields from inside your transform method to be a top-level property. I used as const to keep their types as string literals rather than just string. (typeof transformFields)[number] gives us the union of those strings.
const transformFields = ["id", "name", "email", "createdAt", "role"] as const;
type Transformed = Pick<UserDocument, (typeof transformFields)[number]>
Our UserModel is a Model of UserDocument and it also includes our checkDuplicateEmailError function.
interface UserModel extends Model<UserDocument> {
checkDuplicateEmailError(err: any): any;
}
We should also add the UserDocument generic when we create our Schema, so that this will have the type UserDocument when we access it inside a schema method.
const userSchema = new Schema<UserDocument>({
I got all sorts of typescript errors trying to implement the transform() method including missing index signatures. We can avoid reinventing the wheel here by using the pick method from lodash. I still had problems with the mongoose methods() helper function, but it works fine using the direct assignment approach.
userSchema.methods.transform = function (): Transformed {
return pick(this, transformFields);
};
You could also use destructuring to avoid the index signature issues.
userSchema.methods.transform = function (): Transformed {
const {id, name, email, createdAt, role} = this;
return {id, name, email, createdAt, role};
}
Within your email check function, I added a typeof check to avoid runtime errors from trying to access the property err.code if err is undefined.
if ( typeof err === "object" && err.code === 11000) {
That should fix all your errors.
Playground Link

Categories