Reputation: 3766
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
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!
Upvotes: 2
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