-
-
Notifications
You must be signed in to change notification settings - Fork 502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exact
intersection type support
#419
Comments
Exact
type intersection type supportExact
intersection type support
I've attempted the solutions proposed in #259, the one from @adam-thomas-privitar and one that I proposed in the PR. I don't think matching on array pattern and recursive |
Hi @JasonShin I tried to find a solution but I couldn't make it work. However, after some thought about it, I feel there is something wrong. An intersection type of 2 object type makes sense. e.g. type X = { x: string }
type Y = { y: number }
type XY = X & Y // => equivalent to { x: string; y: number } However, for array type type ArrX = Array<{ x: string }>
type ArrY = Array<{ y: number }>
type ArrXY = ArrX & ArrY // => doesn't feel right that it is equivalent to Array<{ x: string; y: number }>
const xy = [{ x: '', y: 1 }] // however, TypeScript says yes, both field are merged Another example I am not sure what's the correct result is type ObjX = { value: { x: string } }
type ObjY = { value: { y: number } }
type ObjXY = ObjX & ObjY // does this mean it's equivalent to { value: { x: string; y: number } }
const xy: ObjXY = {
value: { x: '', y: 1 } // and it is, TypeScript requires both fields
} I also found TypeScript sometimes doesn't behave consistently, e.g. const test: Array<{ x: string }> & Array<{ y: string }> = []
test.forEach((item) => item.y) // TypeScript complains that item only has `x` and does not have `y` I also found another discussion regards to this issue and it's unclear how it should be interpreted. microsoft/TypeScript#11961 (comment) I'll continue try different ideas but I am afraid there might not be a solution. |
I also found the For the follow type
I am not sure whether I should interpret it as a List that accept both Dog and Cat or it must be either a list of Dog or list of Cat. |
Hi @JasonShin I attempted 2 approach to extract a node from an array and it seems they gives me different result. type ArrayAnd = Array<{ x: string }> & Array<{ z: number }>
type ArrayNode1<T> = T extends Array<infer U> ? U : never // 1st approach, use infer
type ArrayNode2<T> = T extends Array<unknown> ? T[0] : never // 2nd approach, user T[0]
type Attempt1 = ArrayNode1<ArrayAnd> // => { z: number } , looks wrong
type Attempt2 = ArrayNode2<ArrayAnd> // => { x: string; z: number } , looks correct I implement the Exact again with import { Primitive } from 'type-fest'
import { KeysOfUnion } from 'type-fest/source/internal'
type ArrayNode<T> = T extends Array<unknown> ? T[0] : never
type ExactObject<ParameterType extends object, InputType> = ({ [Key in keyof ParameterType]: ParameterType[Key] }
& Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>)
export type Exact<ParameterType, InputType> = ParameterType extends Primitive ? ParameterType
: ParameterType extends Array<unknown> ? Exact<ArrayNode<ParameterType>, ArrayNode<InputType>>[]
: ParameterType extends object ? ExactObject<ParameterType, InputType>
// should never reach the type below as ALL types should be either primitive
// or array or object. However, if TypeScript does introduce a new type, the statement below can guard it
: never
type ParameterType = Array<{ x: string }> & Array<{ z: number }>
const fn = <T extends Exact<ParameterType, T>>(args: T) => args
// correct input
fn([{
x: '',
z: 1,
}])
// missing field
fn([{
z: 1,
}])
// missing field
fn([{
x: '',
}])
// incorrect type
fn([{
x: 1,
z: 1
}])
// excess field
fn([{
x: '',
y: '',
z: 1
}]) It looks like it's working for your use case, could you help me verify whether this works for you too? |
Thanks, @zorji, I just tested your updated {
type ParameterType = Array<{ x: string }> & Array<{ z: number, d: { e: string } }>
const fn = <T extends Exact<ParameterType, T>>(args: T) => args
fn([{
x: '',
z: 1,
d: {
e: 'test',
f: true // allows the excess field `f` for nested object 🤔
}
}])
} Also the updated type breaks nested object's excess fields check in general for non-intersection type type Type = {
body: {
[k: string]: unknown;
code: string;
name?: string;
};
};
const fn = <T extends Exact<Type, T>>(args: T) => args;
{
const input = {body: {code: '', mmm: true}}; // mmm is allowed 🤔
fn(input);
} Sorry, this is super tricky 🤯 . I will try fixing it later as well |
@JasonShin opps, my bad, I missed something in the Please try with the one below type ExactObject<ParameterType extends object, InputType> = ({ [Key in keyof ParameterType]: ExactObject<ParameterType[Key], InputType[Key]> }
& Record<Exclude<keyof InputType, KeysOfUnion<ParameterType>>, never>) |
@zorji Perfect! It works great |
I was playing with @zorji 's
Exact
type and found that it errors for intersection types, but specifically when you construct the intersection type likeArray<{ a: string }> & Array<{ b: string }>
I can only reproduce the error in the following scenario:
As per https://www.reddit.com/r/typescript/comments/m5wvrx/how_to_extract_proper_intersection_type_from_an/ and microsoft/TypeScript#43267
It seems like a workaround for those who have control over type declarations would be writing the intersection type like following:
The only problem is when the user doesn't have control over the underlying module's type declaration but they are providing the weird intersection type that goes
Array<{ ... }> & Array<{...}>
, then they will get the error when trying to useExact
The text was updated successfully, but these errors were encountered: