0

I'm just starting discovering TypeScript and I'm testing the limits. What I'm looking for is a way to make an interface that as fields depending on the value of one of it's property.

For example:

type type RecursiveArray<T> = T | RecursiveArray<T>[];
type allowedTypesString = 'string' | 'email' | 'date' | 'id' | 'number' | 'boolean' | 'object';


interface IOptions {
    type: RecursiveArray<allowedTypesString>,
    required?: boolean,
    defaultValue?: PROVIDED_TYPE,
    expected?: PROVIDED_TYPE[],
    transform?: (value: PROVIDED_TYPE) => any,
    validate?: (value: PROVIDED_TYPE) => boolean,
    regexp?: RegExp,
    min?: number,
    max?: number,
    params?: object,
}

I want IOptions to have:

  • regex property only if type is "string" or "email" .
  • params only if type is "object".
  • min and max only if type is "number, etc...

I saw that I could use discriminated unions like in this thread, but as you can see, the type property is a RecursiveArray<allowedTypesString> which means that it can be a single string, an array of string, an array of array of string, etc...

With unions, I could declare :

interface IOptionsString {
    type: string,
    defaultValue?: string,
    expected?: string[],
    regexp?: RegExp,
}

that will be called if the type is a string. But what if I'm receiving an array of string or an array of array of string ?

Is what I'm doing possible ? Otherwise I'll just handle single array but I want to know if what I'm thinking of is possible in TypeScript.

Thanks for your help !

GaldanM
  • 125
  • 2
  • 12

1 Answers1

2

You can do what you want with conditional types if you parameterize IOptions with the actual type and then turn into an intersection of common types and special ones that depend on the type.

To go back to the original types (i.e. PROVIDED_TYPE in your example) you need to change allowedTypesString into a map from string types to actual types and then use conditional types to reify (i.e. convert from value to type).

type RecursiveArray<T> = T | RecursiveArray<T>[];

interface TypeStringMap {
  'string': string;
  'email': string;
  'date': Date;
  'id': string;
  'number': number;
  'boolean': boolean;
  'object': object;
}

type RecursiveTypes = RecursiveArray<keyof TypeStringMap>;

type HasRegexp<T extends RecursiveTypes> =
  T extends RecursiveArray<'string' | 'email'> ? { regexp: RegExp } : {}

type HasMinMax<T extends RecursiveTypes> =
  T extends RecursiveArray<'number'> ? { min: number, max: number } : {}

type ReifyRecursiveType<T> =
  T extends keyof TypeStringMap ? TypeStringMap[T]
  : (T extends (infer U)[] ? ReifyRecursiveType<U>[] : never)

type IOptions<T extends RecursiveTypes> = {
  type: T;
  expected?: ReifyRecursiveType<T>[];
  defaultValue?: ReifyRecursiveType<T>,
  transform?: <TResult>(value: ReifyRecursiveType<T>) => TResult,
  validate?: (value: ReifyRecursiveType<T>) => boolean,
} & HasRegexp<T> & HasMinMax<T>

type IStringOptions = IOptions<'string'>; // has `regexp` field
type IStringOrEmailOptions = IOptions<('string' | 'email')>; // has `regexp` field
type IEmailArrayArrayOptions = IOptions<'email'[][]>; // has `regexp` field
type INumberOptions = IOptions<'number'>; // has `min` and `max` fields
Dmitriy
  • 2,742
  • 1
  • 17
  • 15
  • Hi, thanks for your answer, I just have a question about the fields where I put "PROVIDED_TYPE", how can I transform the type "string" to the real string type ? Also, how can I tell if it's a string, an array of string or more ? For example, if T is an array of string, I want the `expected` field to be an array of array of string. Is that possible ? – GaldanM Dec 26 '19 at 17:49
  • 1
    @GaldanM I've updated the answer with the information – Dmitriy Dec 27 '19 at 19:14
  • Hi @Dmitry, thanks a lot for your update, I think I understand TypeScript much better now. But this solution still isn't working unfortunately... In fact, the IOptions will be used in another interface like that : `interface IParams { [key: string]: IOptions }` Problem is that `IOptions` ask for a type, so I tried to put `IOptions`, it looks like it's working, but when a create an object of type `IParams`, if I put `type` to "string", the `max` property is allowed when it should not... Thanks a lot for your help ! – GaldanM Jan 06 '20 at 17:23
  • Also, if I put `defaultValue` as ['foo', 'bar'], when type is "string", I don't get an error but `defaultValue` should be of the same type as `type` – GaldanM Jan 06 '20 at 17:58