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 !

1 Answer 1

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
4
  • 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
    Commented Dec 26, 2019 at 17:49
  • 1
    @GaldanM I've updated the answer with the information
    – Dmitriy
    Commented Dec 27, 2019 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<RecursiveTypes>, 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
    Commented Jan 6, 2020 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
    Commented Jan 6, 2020 at 17:58

Not the answer you're looking for? Browse other questions tagged or ask your own question.