Issue
This code is simplified from a CSV writing library:
(I'm using the UnionToIntersection
helper from here: https://stackoverflow.com/a/50375286/163832)
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Spec = 'string' | 'timestamp' | ['nullable', Spec];
type SpecToValue<T> = UnionToIntersection<
T extends 'string' ? string :
T extends 'timestamp' ? Date :
T extends ['nullable', infer InnerT] ? SpecToValue<InnerT> | null :
never
>;
declare function writeValue<T extends Spec>(spec: T, value: SpecToValue<T>): void;
function test(s1: 'string', s2: 'string' | 'timestamp', s3: ['nullable', 'string']) {
writeValue(s1, 'hello');
writeValue(s1, false); // error (good)
writeValue(s1, null); // error (good)
writeValue(s2, 'hello'); // error (good)
writeValue(s2, false); // error (good)
writeValue(s2, null); // error (good)
// Problem: SpecToValue<['nullable', 'string']> resolves to `never` instead of `string | null`.
writeValue(s3, 'hello'); // ERROR (bad; I want this to be allowed)
writeValue(s3, false); // error (good)
writeValue(s3, null); // ERROR (bad; I want this to be allowed)
I think the problem is that the UnionToIntersection
also converts the SpecToValue<InnerT> | null
to an intersection, but I don't want to do that. I want to keep that as a union.
Is there a way to do that?
Edit: In my actual code, I have the "simple" types separated from the "nullable" types; not sure if that makes much of a difference to the solution:
type SimpleSpec = 'string' | 'timestamp';
type Spec = SimpleSpec | ['nullable', SimpleSpec];
type SimpleSpecToValue<T> =
T extends 'string' ? string :
T extends 'timestamp' ? Date :
never;
type SpecToValue<T> =
T extends SimpleSpec ? SimpleSpecToValue<T> :
T extends ['nullable', infer InnerT] ? (SpecToValue<InnerT> | null) : null
(Not a deal-breaker if I have to combine the definitions, though.)
Solution
You don't really want to apply UnionToIntersection<T>
to the results of SpecToValue
, because that's not the level at which you are trying to convert unions to intersections. Instead you really want to compute SpecToValue<T>
for each union member of T
, and then produce the intersection of the result.
Essentially that means you want to apply SpecToValue<T>
inside your UnionToIntersection
-like utility type, and not vice versa:
type SpecToValue<T> =
T extends 'string' ? string :
T extends 'timestamp' ? Date :
T extends ['nullable', infer InnerT] ? SpecToValue<InnerT> | null :
never
type ContravariantSpecToValue<T> =
(T extends any ? ((x: SpecToValue<T>) => void) : never) extends
((x: infer I) => void) ? I : never;
Let's test it:
type Combination = ContravariantSpecToValue<
['nullable', ['nullable', 'timestamp']] | 'timestamp'
>;
//type Combination = Date
Looks good, the result is Date & (Date | null)
(or Date
) and not Date & Date * null
(or never
). And just to be sure it works with your examples:
declare function writeValue<T extends Spec>(
spec: T, value: ContravariantSpecToValue<T>): void;
function test(s1: 'string', s2: 'string' | 'timestamp', s3: ['nullable', 'string']) {
writeValue(s1, 'hello'); // okay
writeValue(s1, false); // error
writeValue(s1, null); // error
writeValue(s2, 'hello'); // error
writeValue(s2, false); // error
writeValue(s2, null); // error
writeValue(s3, 'hello'); // okay
writeValue(s3, false); // error
writeValue(s3, null); // error
}
Also good.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.