Reputation: 24231
Is it possible in Typescript to infer multiple properties to be the same type, or maybe infer the types based on a specific property..
Lets say we have ->
interface Test<T> {
value1: T;
value2: T;
}
const test1:Test<string> = {
value1: 'string1',
value2: 'string1'
}
In the above if I made value1 or value2 into a number it would error, great. But it would be nice to remove the generic <string>
and for it to work out it's based on maybe the first property types value1
.
At first I thought I could use a default for T as any, and it would then later infer the type, but it just keeps as any, as can be seen below we have a string and a number.
interface Test<T = any> {
value1: T;
value2: T;
}
const test1:Test = {
value1: 'string1',
value2: 2
}
Upvotes: 3
Views: 865
Reputation: 330216
As you saw, a generic type parameter default doesn't have the effect you intend. If you refer to the type as Test
without specifying the type parameter as in Test<string>
, the compiler will use the default and interpret it as Test<any>
. It will not infer string
for you. There is a feature request, microsoft/TypeScript#10671, asking for a way to have the compiler infer unspecified type parameters... something like Test<infer>
or Test<*>
. But so far TypeScript does not have this feature.
Instead what you can do is create a generic utility function which returns its input:
const asTest = <T,>(test: Test<T>) => test;
When you call a generic function, TypeScript will try to infer an appropriate type for a test parameter. You can call this function instead of annotating the variable:
const testString = asTest({
value1: "string1",
value2: "string2"
})
// const testString: Test<string>
const testStringNumber = asTest({
value1: "string1",
value2: 2 // error! 'number' is not assignable to 'string'
})
// const testString: Test<string>
This behaves as desired.
Do note though that it's not always obvious whether the compiler will reject a value or synthesize a union type for its type parameter. In testStringNumber
it would have been reasonable for T
to be inferred as string | number
, but the heuristic the compiler uses is that people don't generally want to infer unions of primitive types.
On the other hand, the following code causes a union to be inferred:
const testUnion = asTest({
value1: { a: "" },
value2: { b: "" }
})
/* Test<{
a: string;
b?: undefined;
} | {
b: string;
a?: undefined;
}> */
That type, Test<{a: string; b?: undefined} | {b: string; a?: undefined}>
does accurately describe the type of the value passed in to asTest()
. The compiler's heuristic assumes that unions of object types are more acceptable and expected.
But perhaps you'd have preferred something that complains about such unions, and uses only (say) value1
to infer T
, and then just checks value2
against it. That is, you'd like the T
in value2
to be a non-inferential type parameter usage as requested in microsoft/TypeScript#14829. There is no official direct support for this, but there are some reasonable ways to simulate it. For example:
const asTest = <T, U extends T>(test: { value1: T, value2: U }): Test<T> => test;
This version of asTest
uses two generic type parameters; T
for value1
, and U
for value2
. U
is constrained to be assignable to T
, so the compiler will only accept inputs where value2
has a type that can be assigned to value1
. And the compiler will only infer T
by looking at value1
and not at value2
. It works the same for testString
and testStringNumber
:
const testString = asTest({ value1: "string1", value2: "string2" });
// const testString: Test<string>
const testStringNumber = asTest({
value1: "string1",
value2: 2 // error! 'number' is not assignable to 'string'
})
// const testString: Test<string>
But now testUnion
produces an error:
const testUnion = asTest({
value1: { a: "" },
value2: { b: "" } // error! '{ b: string; }' is not assignable to '{ a: string; }'
})
because {b: ""}
cannot be assigned to the type {a: string}
as inferred from value1
.
Upvotes: 5