I'm trying to write a generic type that takes a type as a parameter (which could be a plain object, an array, a primitive, etc.) and remaps the value types when it is a plain object or an array to add some configuration directives described by a Configuration type.
Let's call that hypothetic modifier Configurable<T>. T could be any complexly nested entity. Book could be a value for T for example :
type Configuration = {
$test: {
option1: boolean;
option2: string;
}
};
type Book = {
id: string;
title: string;
author: string;
related: Array<string>;
};
type Result = Configurable<Book>;
I then want Configurable<Book> to correctly type-check the following expressions where the values could be the actual values or the configuration object :
const expr1: Configurable<Book> = {
id: "1",
title: "Harry Potter",
author: "J.K. Rowling",
related: ["2", "3"]
}
const expr2: Configurable<Book> = {
id: "2",
title: "Harry Potter",
author: {
$test: {
option1: true,
option2: "something"
}
},
related: []
}
const expr3: Configurable<Book> = {
id: "3",
title: "Harry Potter",
author: "J.K. Rowling",
related: ["2", {
$test: {
option1: true,
option2: "something"
}
}]
}
const expr4: Configurable<Book> = {
id: "4",
title: true, // ERROR: should be string or Configuration
author: "J.K. Rowling",
related: ["2", "3"]
}
const expr5: Configurable<Book> = {
id: "5",
title: "Harry Potter",
author: "J.K. Rowling",
related: {
$test: {
option1: true,
option2: "something"
}
} // ERROR: should be an array of (string | Configuration)
}
Nested object or arrays should not be replaceable by Configuration, only where a primitive value is expected (see expr5).
Here is what I tried :
type Configuration = {
$test: {
option1: boolean;
option2: string;
};
};
type Configurable<T> = Record<string, any> extends T
? {
[K in keyof T]: Configurable<T[K]> | Configuration;
}
: T extends Array<infer U>
? Array<Configurable<U>>
: T;
But this makes expr2 and expr3 fail.
If I understand your requirements correctly, you could use this definition of
Configurable:The
objecttype corresponds to any non-primitive, including arrays. IfT extends objectis not true, thenTis a primitive and you want to acceptT | Configuration. IfT extends objectis true, then you map over its properties withConfigurable. This should automatically do the right thing with arrays and tuples, since mapped tuples and arrays produce tuples and arrays.Let's try it out:
The above examples compile with no error as desired. Let's check the errors:
The errors you want are indeed produced. Looks good!
Playground link to code