Having a type Task:
type Task = {
id: string;
prop1: string;
prop2: number;
}
I was trying to properly type an argument which will allow me to express an update to the task:
I came up with two alternatives:
type TaskUpdate1 = Pick<Task, 'id'> & Partial<Omit<Task, 'id'>>;
type TaskUpdate2 = Pick<Task, 'id'> & Partial<Task>;
which, to my eyes, are the same, but two type assertion libraries claim they are different:
import { Expect, Equal } from 'type-testing';
import { assert, _ } from "spec.ts";
type Task = {
id: string;
prop1: string;
prop2: number;
}
type TaskUpdate1 = Pick<Task, 'id'> & Partial<Omit<Task, 'id'>>;
type TaskUpdate2 = Pick<Task, 'id'> & Partial<Task>;
type test_0 = Expect<Equal<TaskUpdate1, TaskUpdate2>>;
var x = {} as TaskUpdate1;
var y = {} as TaskUpdate2;
assert(x, y);
Is there any difference in these types (and if so, what difference? To rephrase it: is there a value that belongs to one and not the other?), or is is a deficiency in the type testing code?
Update
The question obviously on knowledge of type-testing and spec.ts I inlined types from these libraries to make the question more self-contained:
/* type-testing: https://github.com/MichiganTypeScript/type-testing/ */
export type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2)
? true
: false;
export type Expect<T extends true> = Equal<T, true>;
/* spec.ts: https://github.com/aleclarson/spec.ts */
// Give "any" its own class
export class Any {
private _: true = true;
}
type TestExact<Left, Right> =
(<U>() => U extends Left ? 1 : 0) extends (<U>() => U extends Right ? 1 : 0) ? Any : never;
type IsAny<T> = Any extends T ? ([T] extends [Any] ? 1 : 0) : 0;
export type Test<Left, Right> = IsAny<Left> extends 1
? IsAny<Right> extends 1
? 1
: "❌ Left type is 'any' but right type is not"
: IsAny<Right> extends 1
? "❌ Right type is 'any' but left type is not"
: [Left] extends [Right]
? [Right] extends [Left]
? Any extends TestExact<Left, Right>
? 1
: "❌ Unexpected or missing 'readonly' property"
: "❌ Right type is not assignable to left type"
: "❌ Left type is not assignable to right type";
type Assert<T, U> = U extends 1
? T // No error.
: IsAny<T> extends 1
? never // Ensure "any" is refused.
: U; // Return the error message.
export const assert: <Left, Right>(
left: Assert<Left, Test<Left, Right>>,
right: Assert<Right, Test<Left, Right>>
) => Right = () => ({}) as any;
/* My code */
type Task = {
id: string;
prop1: string;
prop2: number;
}
type TaskUpdate1 = Pick<Task, 'id'> & Partial<Omit<Task, 'id'>>;
type TaskUpdate2 = Pick<Task, 'id'> & Partial<Task>;
type test_0 = Expect<Equal<TaskUpdate1, TaskUpdate2>>;
var x = {} as TaskUpdate1;
var y = {} as TaskUpdate2;
assert(x, y);
Those two types are structually equivalent, meaning that for most practical purposes they should behave the same.
You can demonstrate the structural equivalence by writing an identity mapped type to walk through each property and give its resulting type (see How can I see the full expanded contract of a Typescript type? for similar ideas):
type Id<T> = { [K in keyof T]: T[K] }
type TaskUpdate1 = Pick<Task, 'id'> & Partial<Omit<Task, 'id'>>;
type TU1 = Id<TaskUpdate1>;
/* type TU1 = {
id: string;
prop1?: string | undefined;
prop2?: number | undefined;
}*/
type TaskUpdate2 = Pick<Task, 'id'> & Partial<Task>;
type TU2 = Id<TaskUpdate2>;
/* type TU2 = {
id: string;
prop1?: string | undefined;
prop2?: number | undefined;
}*/
So even though TaskUpdate1
and TaskUpdate2
are represented differently, they produce identical TU1
and TU2
types.
As for why Equal
and assert
seem them as being different, it looks like those type equality operators were not intended to check for mere structural equivalence. Instead they are closer to checking that the types are actually represented in equivalent ways by the type checker. That's what How to test if two types are exactly the same and its answers are about. I'm not sure how someone using a type-level equality operators wants intersections like this to behave, and whether or not this would be considered a bug, a design limitation, or an intended feature of the libraries you're using. But in any case I'd say they do not meet your needs and you shouldn't use them here.
Generally speaking I'd recommend just using mutual assignability (or compatibility) as a metric for equality, like
declare let tu1: TaskUpdate1;
declare let tu2: TaskUpdate1;
tu1 = tu2; // okay
tu2 = tu1; // okay
until and unless you find a specific use case in which that is insufficient.