In my application, there are two color formats: hex and rgb. A hex color is stored as a string, and an rgb color is stored as an object that adheres to the following interface: { r: number; g: number; b: number; a: number }
{ r: number; g: number; b: number; a: number }
{ r: number; g: number; b: number; a: number }
. My type definitions look like this:
type Color<T> =
T extends 'hex' ? string :
T extends 'rgb' ? ColorRgb :
never
type ColorRgb = { r: number; g: number; b: number; a: number }
// Infer color value type via color format
type T0 = Color<'hex'> // string ✅
type T1 = Color<'rgb'> // ColorRgb ✅
Here, T0
and T1
are inferred as expected.
I'm having trouble writing a function for which one parameter is narrowed down based on the value of another parameter. The following piece of code is my starting point. It's a function that takes format
and value
parameters. What exactly it does matters not.
What I want to accomplish is this: If I narrow down the value of the format
parameter (eg an if statement checking whether format === 'hex'
), I want TypeScript to somehow infer the type of the value
parameter. When format === 'hex'
, value
should be of type string
; when format === 'rgb'
, value
should be of type ColorRgb
.
function processColor (format: 'hex' | 'rgb', value: string | ColorRgb) {
if (format === 'hex') {
console.log(format, value) // format: 'hex' ✅, value: string | ColorRgb ❌
} else {
console.log(format, value) // format: 'rgb' ✅, value: string | ColorRgb ❌
}
}
One approach I followed was using function overloads .
function processColor2 (format: 'hex', value: string);
function processColor2 (format: 'rgb', value: ColorRgb);
function processColor2 (format: 'hex' | 'rgb', value: string | ColorRgb) {
if (format === 'hex') {
console.log(format, value) // format: 'hex' ✅, value: string | ColorRgb ❌
} else {
console.log(format, value) // format: 'rgb' ✅, value: string | ColorRgb ❌
}
}
It appears to me that function overloads are a way to specifically make calling a function more restrictive as this implementation doesn't change the situation with regards to my goal at all.
Another approach I looked into was using a generic function with constraints .
function processColor3<T extends 'hex' | 'rgb'> (format: T, value: Color<T>) {
if (format === 'hex') {
console.log(format, value) // format: T extends 'hex' | 'rgb' ❌, value: Color<T> ❌
} else {
console.log(format, value) // format: T extends 'hex' | 'rgb' ❌, value: Color<T> ❌
}
}
Here, I'm actually surprised to see that I'm losing type information within the function definition. Despite narrowing down the format
parameter to be 'hex'
in the if
branch, its type is now reported as T extends 'hex' | 'rgb'
T extends 'hex' | 'rgb'
. I might be misunderstanding what exactly conditional types can accomplish or I might be using them incorrectly.
How can I tell TypeScript to infer a function parameter type based on the narrowed-down type of another function parameter?
Both overloads and generic conditional types are, as you noticed, really only good for guaranteeing type safety for the caller of the function. Inside the implementation of the function, the compiler loses information necessary to maintain type safety.
For overloads this is intentional; it is the implementer's responsibility to satisfy the call signatures' contracts and the compiler doesn't really try to verify it. There was a suggestion to change this at microsoft/TypeScript#13235 but this was closed as too complex.
For generic conditional types the issue is that inside the function implementation, any generic type parameters are unspecified . Conditional types that depend on unspecified type parameters are mostly deferred and not evaluated. The compiler therefore doesn't really see any concrete values as assignable to such types; there is an open issue asking for some way to deal with this, at least for return types, at microsoft/TypeScript#33912 . Even for non-conditional types, unspecified generics tend not to be very easy to manipulate. One major barrier is that testing a value of type T
with a type guard (eg, format === 'hex'
) does not do the sort of control flow analysis you get for specific types. In your case, format
is not narrowed at all. See microsoft/TypeScript#13995 for more information.
Another barrier to fixing this in general is that the compiler does not keep track of correlations between multiple expressions of union types. In your code, you want to say that format
and value
are both union of types, but they are not independent . With each union having two members, the compiler will always assume that the pair [format, value]
can have up to four possible types corresponding to each member from each union, as if they were independent. Even though you know that the type of format
is correlated with the type of value
so that only two possible types exist for [format, value]
, the compiler just does not notice such things. I have an issue at microsoft/TypeScript#30851 wishing for some support for this, but compilers don't run on wishes.
So what can you do? All you can do in the general case is to use type assertions or the equivalent to just tell the compiler about things it can't verify. For example, you could make an assertion function that does nothing at runtime but tells the compiler to narrow the type of its parameter to some specified type, like this:
function compileTimeAssert<T>(x: any): asserts x is T {}
And then inside your implementation you can use it:
if (format === 'hex') {
compileTimeAssert<string>(value); // I'm telling the compiler this
console.log(format, value.toUpperCase());
} else {
compileTimeAssert<ColorRgb>(value); // I'm telling the compiler this
console.log(format, value.r);
}
Such assertions are only as type safe as you make them; the responsibility for guaranteeing type safety has been shifted from the compiler to you, so be careful.
In the specific example you've given, I'd be inclined to change Color
from a conditional type to an object property lookup like this:
interface ColorMap {
hex: string;
rgb: ColorRgb;
}
type Color<T extends keyof ColorMap> = ColorMap[T];
It's not necessary to do this, but it's easier for the compiler to reason about. Unless there's a compelling reason why you want to be able to write Color<Date>
and have it evaluate to never
, I'd do it that way.
Then, I'd suggest thinking of processColor()
as a function whose rest parameter is a tuple type . So instead of (a: A, b: B)=>void
, it's (...args: [A, B])=>void
. Your constraint corresponds to having the rest parameter type be a union of tuples. You can make the compiler calculate this type:
type ProcessColorParams =
{ [K in keyof ColorMap]: [format: K, value: Color<K>] }[keyof ColorMap];
// type ProcessColorParams =
// [format: "hex", value: string] | [format: "rgb", value: ColorRgb]
You can see that ProcessColorParams
is a union where either the first element is "hex"
and the second element is string
, or where the first element is "rgb"
and the second element is ColorRgb
. And since the first element is a string literal type , this union is a discriminated union , where you can check the first element and the compiler will automatically narrow down the remaining elements.
So processColor
can become tihs:
function processColor(...args: ProcessColorParams) {
if (args[0] === 'hex') {
console.log(args[0], args[1].toUpperCase());
} else {
console.log(args[0], args[1].r);
}
}
Now the constraint is enforced from the call side and understood in the implementation. Yes, args[0]
and args[1]
are uglier than format
and value
, but if you break apart a discriminated union into separate variables you'll run into the problem with correlation I mentioned above. If you want to do this, do it after you discriminate the union:
function processColor(...args: ProcessColorParams) {
if (args[0] === 'hex') {
const [format, value] = args;
console.log(format, value.toUpperCase());
} else {
const [format, value] = args;
console.log(format, value.r);
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.