export type Parser = NumberParser | StringParser;
type NumberParser = (input: string) => number | DiplomacyError;
type StringParser = (input: string) => string | DiplomacyError;
export interface Schema {
[key: string]: Parser | Schema;
}
export type RawType<T extends Schema> = {
[Property in keyof T]: T[Property] extends Schema
? RawType<T[Property]>
: ReturnType<Exclude<T[Property], Schema>>;
};
// PersonSchema is compliant the Schema interface, as well as the address property
const PersonSchema = {
age: DT.Integer(DT.isNonNegative),
address: {
street: DT.String(),
},
};
type Person = DT.RawType<typeof PersonSchema>;
Sadly type Person is inferred as:
type Person = {
age: number | DT.DiplomacyError;
address: DT.RawType<{
street: StringParser;
}>;
}
Instead I would have liked to get:
type Person = {
age: number | DT.DiplomacyError;
address: {
street: string | DT.DiplomacyError;
};
}
What am I missing?
The difference between the
Persondisplayed and the type you expected is pretty much just cosmetic. The compiler has a set of heuristic rules it follows when evaluating and displaying types. These rules have changed over time and are occasionally tweaked, such as the "smarter type alias preservation" support introduced in TypeScript 4.2.One way to see that the types are more or less equivalent is to create both of them:
And then see that the compiler considers them mutually assignable:
The fact that those lines did not result in a warning means that, according to the compiler, any value of type
Personis also a value of typeDesiredPerson, and vice versa.So maybe that's enough for you.
If you really care about how the type is represented, you can use techniques such as described in this answer:
If I compute
ExpandRecursively<Person>, it walks down throughPersonand explicitly writes out each of the properties. Assuming thatDiplomacyErroris this (for want of a minimal reproducible example in the question):Then
ExpandRecurively<Person>is:which is closer to what you want. In fact, you could rewrite
RawTypeto use this technique, like:which is exactly the form you wanted.
(Side note: there is a naming convention for type parameters, as mentioned in this answer. Single capital letters are preferred over whole words, so as to distinguish them from specific types. Hence, I have replaced
Propertyin your examples withKfor "key". It might seem paradoxical, but because of this convention,Kis more likely to be immediately understood by TypeScript developers to be a generic property key thanPropertyis. You are, of course, free to continue usingPropertyor anything else you like; it's just a convention, after all, and not some sort of commandment. But I just wanted to point out that the convention exists.)Playground link to code