I have two interfaces, Workflow and WorkflowVersion
workflow.model.ts
import { WorkflowVersion } from './workflow-version.model';
export interface Workflow{
name: string;
ID: number;
workflowVersions: WorkflowVersion[];
}
workflow-version.model.ts
export interface WorkflowVersion{
versionID: number;
lastPublished: string;
environmentID: number;
}
I'd like to create an interface that 'flattens' the two. This new interface, WorkflowFlat, should contain all non-object type properties of both interfaces. Currently I have this:
workflow-flat.model.ts
export interface WorkflowFlat {
name: string;
ID: number;
versionID: number;
lastPublished: string;
environmentID: number;
}
The model above achieves this, but it feels repetitive. If I want to add a description property to WorkflowVersion down the road, I'd have to remember to also add it to WorkflowFlat. Is there any way to make the model automatically take all properties from the two interfaces and then create a new interface with the non-object ones?
Aaron Beall's answer is close (sorry I can't comment yet), but it doesn't properly remove all the keys that extends object, so they'll be required by the final type as key: never which is not wanted.
The following snippet achieves the objective by reusing a type-level helper from the awesome tycho01/typical repo:
export type NonMatchingPropNames<T, X> = { [K in keyof T]: T[K] extends X ? never : K }[keyof T];
export type NonMatchingProps<T, X> = Pick<T, NonMatchingPropNames<T, X>>;
type PrimitiveValuesOf<T> = NonMatchingProps<T, object>;
type A = PrimitiveValuesOf<Workflow & WorkflowVersion>;
const a: A = {
name: '',
ID: 0,
versionID: 1,
lastPublished: '',
environmentID: 2
}; // OK
Related
I have following Nestjs DTO class:
// create-job-offer.dto.ts
import { IsOptional, IsNumber } from 'class-validator';
export class CreateJobOfferDto {
#IsNumber()
#IsOptional()
mentorId: number;
#IsNumber()
#IsOptional()
companyId: number;
}
I want to obtain the list of class properties: ['mentorId', 'companyId'].
I tried so far in a controller without success following methods:
Object.getOwnPropertyNames(new CreateJobOfferDto());
Object.getOwnPropertyNames(CreateJobOfferDto);
Object.getOwnPropertySymbols(new CreateJobOfferDto());
Object.getOwnPropertySymbols(CreateJobOfferDto);
Object.getOwnPropertyDescriptors(CreateJobOfferDto);
Object.getOwnPropertyDescriptors(new CreateJobOfferDto());
Object.getPrototypeOf(CreateJobOfferDto);
Object.getPrototypeOf(new CreateJobOfferDto());
If I add a method, or vars in a constructor, I can get them, but not the properties.
The reason why I want to achieve this is, I am using Prisma and React, and in my React app I want to receive the list of class properties so that I can generate a model form dynamically.
There is no easy direct way to get the list of properties of a DTO class, or
more comprehensively, any type of class that has only properties.
The reason is that whenever you defined a property without any values, it becomes disappears after compiling to javascript.
Example:
// typescript
class A {
private readonly property1: string;
public readonly property2: boolean;
}
it compiles to this:
// javascript
"use strict";
class A {}
In order to achieve this goal, you need to write a custom decorator. like this:
const properties = Symbol('properties');
// This decorator will be called for each property, and it stores the property name in an object.
export const Property = () => {
return (obj: any, propertyName: string) => {
(obj[properties] || (obj[properties] = [])).push(propertyName);
};
};
// This is a function to retrieve the list of properties for a class
export function getProperties(obj: any): [] {
return obj.prototype[properties];
}
Imagine I have a DTO class for a user, like this:
import { getProperties } from './decorators/property.decorator';
export class UserDto {
#Property()
#IsNotEmpty()
firstName: string;
#Property()
#IsEmail()
#IsOptional()
public readonly email: string;
}
and I want to get all properties in a list, so we need to call the getProperties method which we defined earlier, so:
import { UserDto } from './dtos/user.dto';
getProperties(UserDto); // [ 'firstName', 'email' ]
You can also use an npm module ts-transformer-keys.
A TypeScript custom transformer which enables to obtain keys of given type.
How to use:
import { keys } from 'ts-transformer-keys';
interface Props {
id: string;
name: string;
age: number;
}
const keysOfProps = keys<Props>();
console.log(keysOfProps); // ['id', 'name', 'age']
In my Project, I am using a mapped type (with strings as keys):
export type ConfigurableFieldsType<T extends string[]> = {
[field in keyof T]: string
}
In the following method, I am using the type (this.configurableFields is a ConfigurableFieldsType object):
private getConfigurableAttribute(attribute: C[number]): string {
return this.configurableFields[attribute]
}
However, I am getting the error:
Type 'C[number]' cannot be used to index type 'ConfigurableFieldsType<C>'.
You can find the complete example (with reproducible error) in this TS Playground or in the following:
export type ProductType<T extends string[]> = {
// more variables
configurableFields: ConfigurableFieldsType<T>
}
export type ConfigurableFieldsType<T extends string[]> = {
[field in keyof T]: string
}
export class Product<C extends string[]> implements ProductType<C> {
constructor(
// more public parameters
public configurableFields: ConfigurableFieldsType<C>,
) {}
private getConfigurableAttribute(attribute: C[number]): string {
return this.configurableFields[attribute]
}
}
You intend ConfigurableFieldsType<["a", "b", c"]> to evaluate to something like {a: string, b: string, c: string}, but your current definition would result in [string, string, string]. That's because {[K in keyof T]: string} results in a mapped array type; T is an arraylike type, and keyof T is thus the array keys, like "push" or "length".
Instead, you want the keys of your type to be elements of T. That is, the types you get when you index into T with a numeric index... so instead of keyof T, you want T[number]:
export type ConfigurableFieldsType<T extends string[]> = {
[K in T[number]]: string
}
type Example = ConfigurableFieldsType<["a", "b", "c"]>
/* type Example = {
a: string;
b: string;
c: string;
} */
And now things work as you intend:
private getConfigurableAttribute(attribute: C[number]): string {
return this.configurableFields[attribute] // okay
}
Playground link to code
Language=Typescript
I want to use the aggregation of 2 interfaces as the value of an indexable-type in a 3rd interface.
Interface 1:
export interface Employee {
id: string
name: string
}
Interface 2:
export interface Department {
department: string
}
Now I want to write an interface equivalent of this:
export interface EmployeeDetails {
employees: {
[key: string]: {
employeeDetails: EmployeeWithDepartment
}
}
}
where EmployeeWithDepartment is:
export interface EmployeeWithDepartment extends Employee {
departmentDetails: Department
}
Is there a way I can create the EmployeeDetails interface without actually creating EmployeeWithDepartment? Some way to include both Employee and Department at once in the EmployeeDetails interface?
PS: I've been using JS & TypeScript only for a week now, so I may not be aware of some concepts that can easily accomplish this.
I believe that what you are looking for is a type intersection, the & opertator. It combines all properties of two types.
For example:
interface A { a: number }
interface B = { b: string }
type C = A & B // { a: number, b: string }
To use that here in your types, you could do something like:
export interface EmployeeDetails {
employees: {
[key: string]: {
employeeDetails: Employee & { departmentDetails: Department }
}
}
}
Playground
This is probably a good page to read: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#interfaces
I am trying to create a function that looks like this:
export function getModel<T extends object>(model: new () => T, properties: { [key: keyof T]: any }): T {
}
The issue that I am having is that key errors saying:
An index signature parameter type must be 'string' or 'number'.
Basically what this is supposed to do is take a class reference, and you should be able to pass a list of the properties that are in the class reference to the second parameter.
export class A {
public b: string;
public c: boolean;
}
getModel(A, { b: 'cat', c: false});
So, What I would like is for the keys to be a list of the properties in the class within the object. How can this be done?
I have a typescript interface like so:
interface DataSku {
fields: {
sku: string;
}
}
interface PostProduct {
fields: {
title: string;
skus: DataSku[];
}
}
Now I want to extend the Product interface so that each object in the skus array has an extra field. So I tried this:
interface Sku extends DataSku {
stripe: Stripe.skus.ISku;
}
interface Product extends PostProduct {
fields: PostProduct['fields'] & {
skus: Sku[];
}
}
In my code, I try a loop like so:
(product as Product).fields.skus.forEach(sku => console.log(sku.stripe));
This throws the following Typescript error:
Property 'stripe' does not exist on type 'DataSku'.
Have I extended the interface wrong? The console does output my stripe object as expected, so it's just Typescript that isn't happy with the definition.
A much more elegant (and working) approach for this would be to use generics.
Update PostProduct with a generic parameter that's used for the type of fields.skus:
interface PostProduct<T extends DataSku = DataSku> {
fields: {
title: string;
skus: T[];
}
}
T extends DataSku means the type has to be a subtype of DataSku. And we can even set a default value for T so PostProduct can also be used without having to specify the generic parameter.
Now, for Product we just have to pass Sku as generic parameter:
interface Product extends PostProduct<Sku> {}
Playground
That said, if you want to it without generics and without modifying DataSku and PostProduct. You could do this:
interface Product extends PostProduct {
fields: Omit<PostProduct['fields'], 'skus'> & {
skus: Sku[];
}
}
Playground