pouya
pouya

Reputation: 3766

Object literal which implements an interface which in turn uses a generic method

I have the following custom types and interfaces:

export type MultiMedia = 'image' | 'audio' | 'video';
export type FieldType = 'string' | 'number' | 'boolean' | MultiMedia;

export interface Field {
  name: string,
  label: string,
  type: FieldType,
  validator: <T>(val: T) => boolean,
  bounds: { lower: number; upper: number }
}

export interface Theme {
  title: string,
  logoPath: string,
  tags: string[],
  fields: Field[]
}

Since field type is different from one to the other, i have defined validator as a generic method in Field interface. When i want to make an object literal which implements Field interface typescript complains

Type '(val: string) => boolean' is not assignable to type '(val: T) => boolean.

const fields: Field[] = [
  {
    name: "firstName",
    label: "First Name",
    type: "string",
    bounds: { lower: 1, upper: 1 },
    validator: (val: string) => {
     return val.length > 20;
    }
  }

I can change Field interface as follows to solve the problem.

export interface Field<T> {
  name: string,
  label: string,
  type: FieldType,
  validator: (val: T) => boolean,
  bounds: { lower: number; upper: number }
}

But then typescript complains about fields property in Theme interface.

Generic Type 'Field' requires 1 type argument(s).

Upvotes: 1

Views: 356

Answers (2)

jcalz
jcalz

Reputation: 328292

I think the issue here is that Field is fundamentally a union, since you want it to be either a string field, or a number field, etc etc. The way I'd do this is make a generic SpecificField<F> interface that represents the constraint from each field type F to its validator, and then make Field a type alias to the union of all SpecificField<F> types:

First let's define the field mapping so we know what the validator should accept

interface FieldMapping {
  image: HTMLImageElement; // guessing
  audio: HTMLAudioElement; // guessing
  video: HTMLVideoElement; // guessing
  string: string;
  number: number;
  boolean: boolean;
}

And now the SpecificField<F> interface:

export interface SpecificField<F extends FieldType> {
  name: string;
  label: string;
  type: F;
  validator: (val: FieldMapping[F]) => boolean;
  bounds: { lower: number; upper: number };
}

You can see how SpecificField<string> will have type of "string" and a validator that accepts only string values. Then we make Field be a union, which can happen like this:

type Field = { [F in FieldType]: SpecificField<F> }[FieldType];

This uses a mapped type to get each field as a property and then gets the union by looking up all the properties. You can verify that it evaluates to:

type Field = SpecificField<"string"> | SpecificField<"number"> |
  SpecificField<"boolean"> | SpecificField<"image"> | SpecificField<"audio"> | 
  SpecificField<"video">

Then you can verify that the following works:

const fields: Field[] = [
  {
    name: "firstName",
    label: "First Name",
    type: "string",
    bounds: { lower: 1, upper: 1 },
    validator: (val: string) => {
      return val.length > 20;
    }
  }
];

Okay, hope that helps; good luck!

Link to code

Upvotes: 2

Austin Fahsl
Austin Fahsl

Reputation: 378

Your last example will work, except you need to declare the type of T when you use the Field interface (both in the Theme interface declaration and in the constant fields declaration), not when validator is declared:

export type MultiMedia = 'image' | 'audio' | 'video';
export type FieldType = 'string' | 'number' | 'boolean' | MultiMedia;

export interface Field<T> {
  name: string,
  label: string,
  type: FieldType,
  validator: (val: T) => boolean,
  bounds: { lower: number; upper: number }
}

export interface Theme {
  title: string,
  logoPath: string,
  tags: string[],
  fields: Field<string>[]
}

const fields: Field<string>[] = [
  {
    name: "firstName",
    label: "First Name",
    type: "string",
    bounds: { lower: 1, upper: 1 },
    validator: (val: string) => {
     return val.length > 20;
    }
  }
]

Edit:

Also, if you want to constrain the type of T to just be one of FieldType, then you can do:

export interface Field<T extends FieldType> {
  name: string,
  label: string,
  type: FieldType,
  validator: (val: T) => boolean,
  bounds: { lower: number; upper: number }
}

Upvotes: 0

Related Questions