Keith
Keith

Reputation: 24231

Infer type for multiple properties to be the same

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

Answers (1)

jcalz
jcalz

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.


Playground link to code

Upvotes: 5

Related Questions