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?
Related
I try to implement a class that extends Array and properly overrides 'map' method. the items in the array must extend a certain type.
implementation without a constrain on the items is working as expected:
export class MyClass<T> extends Array<T> {
constructor(...items: T[]) {
super(...items);
// Set the prototype explicitly. (typescript does not allow to extend base classes like Array)
// see: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
Object.setPrototypeOf(this, VectorArr.prototype);
}
map<K>(callback: (value: T, index: number, array: T[]) => K, thisArg?: any): MyClass<K> {
const mapped = super.map(callback, thisArg);
return new MyClass(...mapped);
}
}
now lets say each item in the array must be extend Vector type:
interface MyVector {}
export class MyClass<T extends MyVector> extends Array<T> {
constructor(...items: T[]) {
super(...items);
// Set the prototype explicitly. (typescript does not allow to extend base classes like Array)
// see: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
Object.setPrototypeOf(this, VectorArr.prototype);
}
// TS2416: Property 'map' in type 'MyClass<T>' is not assignable to the same property in base type 'T[]'. Types of parameters 'callback' and 'callbackfn' are incompatible. Type 'U' is not assignable to type 'MyVector'. lib.es5.d.ts(1441, 9): This type parameter might need an `extends MyVector` constraint.
// v
map<K extends MyVector>(callback: (value: T, index: number, array: T[]) => K, thisArg?: any): MyClass<K> {
const mapped = super.map(callback, thisArg);
return new MyClass(...mapped);
}
}
now we got an error: basically, typescript says that because the base class(the native Array) implementation does not have to extend MyVector(Da!) my implementation is wrong(not compatible with the base class method - generic type K from my implementation is extending MyVector but generic type U from the native implementation does not have this constrain).
well, seriously? I try to enforce a more specific type than the native array, why does typescript suggest me to extend the base class method implementation so it will extend MyVector as well?
how do I make typescript stop yelling it me?
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
In my current typescript project I'm trying to create a mixin so that I can create multiple child classes that inherit from different base classes.
It all works well, but I can't seem to figure out how to tell typescript that the new derived class has different arguments than the base class. Here's an example that illustrates what I'm trying to do here
interface ConstructorFoo {
bar: string,
}
class Foo {
public bar: string
constructor({ bar }: ConstructorFoo) {
this.bar = bar
}
}
interface ConstructorBaz extends ConstructorFoo {
qux: string
}
type FooType = new (...args: any[]) => Foo
const quxMixin = <T extends FooType>(base: T) => {
return class Baz extends base {
public qux: string
constructor (...args: any[]) {
super(...args)
const { qux } = args[0] as ConstructorBaz
this.qux = qux
}
}
}
const FooBaz = quxMixin(Foo)
const q = new FooBaz({
bar: '1',
qux: '2' // Argument of type '{ bar: string; qux: string; }' is not assignable to parameter of type 'ConstructorFoo'.
// Object literal may only specify known properties, and 'qux' does not exist in type 'ConstructorFoo'.
})
But I get the following error as I don't know how to specify class Baz has different argument types:
Argument of type '{ bar: string; qux: string; }' is not assignable to parameter of type 'ConstructorFoo'.
Object literal may only specify known properties, and 'qux' does not exist in type 'ConstructorFoo'.
Thanks for your help and here's a playground link detailing exactly what I want to do
Try this:
/** A constructor that constructs a T using the arguments A */
type Constructor<T = any, A extends any[] = any[]> = new (...args: A) => T
/** Exclude the first element of an array */
type Tail<T extends any[]> = T extends [any, ...infer U] ? U : never
interface Qux {
qux: string
}
/** Add the Qux type to the first item in an array */
// If the T is empty, T[0] will be never and T[0] & Qux will also be never, so
// this needs to check if the array is empty
type AddQux<T extends any[]> = T extends [] ? [Qux] : [T[0] & Qux, ...Tail<T>]
// quxMixin accepts a constructor base and returns another constructor
const quxMixin = <T extends Constructor>(base: T): Constructor<
// that constructs the original class with the qux property
InstanceType<T> & Qux,
// using the same arguments as the original constructor except that the first
// parameter includes the qux property
AddQux<ConstructorParameters<T>>
> => {
return class Baz extends base {
public qux: string
constructor (...args: any[]) {
super(...args)
const { qux } = args[0] as Qux
this.qux = qux
}
}
}
const FooBaz = quxMixin(Foo)
const q = new FooBaz({ bar: '1', qux: '2' })
q.qux // string
This uses the utility types InstanceType and ConstructorParameters:
/** Obtain the return type of a constructor function type */
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any
/** Obtain the parameters of a constructor function type in a tuple */
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never
Playground link
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
I create my typescript definitions for some reactJS components and I see that in react.d.ts file there is 2 interfaces:
interface ComponentClass<P> {
new(props?: P, context?: any): Component<P, ComponentState>;
propTypes?: ValidationMap<P>;
contextTypes?: ValidationMap<any>;
childContextTypes?: ValidationMap<any>;
defaultProps?: P;
displayName?: string;
}
and:
interface ClassicComponentClass<P> extends ComponentClass<P> {
new(props?: P, context?: any): ClassicComponent<P, ComponentState>;
getDefaultProps?(): P;
}
I see that ClassicComponentClass extends ComponentClass, but when I should use one of these? (when creating definition for component) Does this depends on how the component was created?
I think you are missing what ComponentClass is.
Here's a short example:
interface MyClassClass {
new (): MyClass;
}
class MyClass {}
let ctor: MyClassClass = MyClass;
let instance: MyClass = new ctor();
In this example MyClass is like React.Component and MyClassClass is React.ComponentClass.
The actual instances in your case are the components and not the component class, and you should use that.
If you don't want to specify a state then you can simply do:
React.Component<ReachWhateverProps, {}>
Edit
First, in the future if your comment includes code (that spans across a few lines) then just edit your question and add the code and asking the follow up questions, and just add a comment saying that you've edited your question, that will make it much easier to understand the code.
As for the difference between the class vs instance, I think that the best example is the javascript Array.
If you look at the definition (in lib.d.ts) you'll see (depending on whether it's ES5 or ES6):
interface Array<T> {
// array instance methods
}
interface ArrayConstructor {
new (arrayLength?: number): any[];
new <T>(arrayLength: number): T[];
new <T>(...items: T[]): T[];
(arrayLength?: number): any[];
<T>(arrayLength: number): T[];
<T>(...items: T[]): T[];
isArray(arg: any): arg is Array<any>;
prototype: Array<any>;
}
declare var Array: ArrayConstructor;
(from the ES5 lib.d.ts)
As you can see all of the instance member/methods (such as length, push etc) are in the Array<T> interface, the ctor functions and static class functions (such as Array.isArray) are in the ArrayConstructor definition.
In javascript class ctors are just functions that are called using the new keyword, so this:
class A {
x: number;
constructor(x: number) {
this.x = x;
}
}
compiles into:
var A = (function () {
function A(x) {
this.x = x;
}
return A;
}());
So A is basically just a function, and in order to create an instance you simply:
let a: A = new A(5);
So the ctor interface is: { new (x: number): A }, or:
interface AConstructor {
new (x: number): A;
}
As for react, it is recommended to have instance data in the props or state and not as class members.
The reason for this is that the Component lifecycle is only aware of those and react to changes in them.
So I'd do something like:
interface MyComponentProperties {
id: string;
name: string;
}
class MyComponent extends React.Component<MyComponentProperties, {}> {
render() {
return <div className={ this.props.id }>{ this.props.name }</div>;
}
}
let myComponent = <MyComponent id="mc4" name="my component 4" />