TypeScript cannot express Translations as a specific type. What you'd need is a different sort of generic type that most languages with generics doesn't have: a so-called existentially quantified generic type, or sometimes just existential type. In your Translations type, you have a type parameter T... but you don't want to have to specify it. Instead, you want to say "the person who supplies a Translations value should be allowed to specify T to whatever they want; all I care or know about is that such a T exists." Maybe such a type definition would look like this:
/* NOT SUPPORTED
type Translations = <exists T extends Record<keyof T, string>>(
{ english: T } & { [K in Language]?: Partial<T> }
);
*/
But of course, you can't do this. There is no exists keyword in TypeScript and no direct method to quantify generic type parameters this way. There's a feature request at microsoft/TypeScript#14466 for existential types, but it's not clear if that feature will ever be implemented.
There are ways to emulate existential types in TypeScript, but they are a bit convoluted and strange to use. Instead of trying to do this, let's give up on existential types and instead look at possible workarounds.
The most straightforward workaround is to define Translations<T> as a generic constraint on the type, with a type parameter T:
type Translations<T extends Record<keyof T, string>> =
{ english: T } & { [K in Language]?: Partial<T> };
To avoid requiring the user to specify T in a type annotation, you can make a helper function:
const asTranslations = <T extends Record<keyof T, string>>(
translations: Translations<T>) => translations;
This identity function just returns its input, so at runtime it's essentially a no-op. But by constraining the input to Translations<T>, we are asking the compiler to infer the generic type T from the input. If the call compiles with no errors, then we know that the input is valid:
const translations = asTranslations({
english: {
foo: "foo",
bar: "bar"
},
german: {
foo: "föö"
}
}); // okay
/* const translations: Translations<{
foo: string;
bar: string;
}> */
Here the compiler has inferred that T is {foo: string, bar: string} and therefore translations is inferred to be of type Translations<{foo: string, bar: string}>.
If you make a mistake, the compiler will warn you about it:
const badTranslations = asTranslations({
english: {
foo: "foo",
bar: "bar"
},
german: {
foo: "föö",
baz: "baß" // error!
// ~~~~~~~~~~
// Type '{ foo: string; baz: string; }' is not assignable
// to type Partial<{ foo: string; bar: string; }>'
// Object literal may only specify known properties, and 'baz'
// does not exist in type 'Partial<{ foo: string; bar: string; }>'
}
});
In this case too, the compiler infers {foo: string; bar: string} for T. But this time, there is an excess property warning on the baz property of german, because baz is neither foo nor bar.
(Note that excess property warnings only happen with object literals. If you need to copy your object literal to a variable before calling asTranslations() on it, you won't get the warning. If that use case is important, you can switch to a different definition for Translations<T>, but I won't bother going into that here unless there's some need for it. It's in the linked Playground Code example below anyway.)
One side effect of this workaround is that you will be required to make anything that deals with a Translations<T> type generic itself. You can plausibly free up end users from having to specify T, but your code base will still end up dragging around an extra type parameter. For example, a function which ideally would look like function saveTranslations(translations: Translations, filename: string): void {} will now look like function saveTranslations<T extends Record<keyof T, string>>(translations: Translations<T>, filename: string): void {}.
For that reason, it might be best to only require such a generic type in a validation function that accepts the input, and then in your internal library code you widen the type to something less accurate but easier to use:
function userFacingFunction<T extends Record<keyof T, string>>(
translations: Translations<T>) {
internalLibraryFunction(translations);
}
type AnyTranslations = Translations<Record<string, string>>;
// not generic anymore
function internalLibraryFunction(translations: AnyTranslations) {
// do something
}
Playground link to code