Skip to content

Commit

Permalink
fix: Add support of union types for arrays, tuples, objects and primi…
Browse files Browse the repository at this point in the history
…tive in isExact (#345)

* test: 🧪 add union type for isExact

* test: 🧪 more combinations

* test: 🧪 object union type

* fix: 🧪 Exact

Add support for primitive exact

* docs: 📄 changeset

* test: 🧪 object undefined union properties

* test: 🧪 isExact

Added more test cases for readonly object and unions

* feat: 🧪 Exact

* fix: 🧪 And

* test: ➕ add test arrays

* feat: ➕ add ArrayExact

Fixed testArray for isExact

* fix: 🧪 isExact

* test: 🧪 testEnums

* docs: 📄 changeset
  • Loading branch information
Beraliv committed Apr 28, 2024
1 parent 72edf30 commit 25f3f60
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-eels-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ts-essentials": patch
---

Add support of union types for arrays, tuples, objects and primitive in `isExact`
95 changes: 92 additions & 3 deletions lib/exact/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
export type Exact<Type, Shape> = Type extends Shape
? Exclude<keyof Type, keyof Shape> extends never
? Type
import { AnyRecord } from "../any-record";
import { IsNever } from "../is-never";

type IsUnion<TUnion> = UnionToTuple<TUnion>["length"] extends 1 ? false : true;

type UnionToFunctionInsertion<TUnion> = (TUnion extends any ? (arg: () => TUnion) => any : never) extends (
arg: infer TParam,
) => any
? TParam
: never;

type UnionToTuple<TUnion> = UnionToFunctionInsertion<TUnion> extends () => infer TReturnType
? [...UnionToTuple<Exclude<TUnion, TReturnType>>, TReturnType]
: [];

type ExactUnionLength<
TValue,
TShape,
TValueLength = UnionToTuple<TValue>["length"],
TShapeLength = UnionToTuple<TShape>["length"],
> = TValueLength extends TShapeLength ? true : false;

type Xor<T, U> = T extends true ? (U extends true ? true : false) : U extends false ? true : false;

type And<TTuple> = TTuple extends [infer Head, ...infer Rest]
? Head extends true
? And<Rest>
: false
: TTuple extends []
? true
: false;

type ObjectKeyExact<TValue, TShape> = And<
[IsNever<Exclude<keyof TValue, keyof TShape>>, IsNever<Exclude<keyof TShape, keyof TValue>>]
>;

type ObjectValueDiff<TValue, TShape> = {
[TKey in keyof TValue]: Exclude<TValue[TKey], TShape[TKey & keyof TShape]>;
}[keyof TValue];

type ObjectValueExact<TValue, TShape> = And<
[IsNever<ObjectValueDiff<TValue, TShape>>, IsNever<ObjectValueDiff<TShape, TValue>>]
>;

type ObjectExact<TValue, TShape> = [TValue] extends [TShape]
? And<
[
Xor<IsUnion<TValue>, IsUnion<TShape>>,
ExactUnionLength<TValue, TShape>,
ObjectKeyExact<TValue, TShape>,
ObjectValueExact<TValue, TShape>,
]
> extends true
? TValue
: never
: never;

type IsArray<TValue> = [TValue] extends [readonly any[]] ? true : false;

type IsReadonly<TArray> = Readonly<TArray> extends TArray ? true : false;

type SameLength<TValue extends readonly any[], TShape extends readonly any[]> = IsNever<
PrimitiveExact<TValue["length"], TShape["length"]>
> extends true
? false
: true;

type ArrayExact<TValue extends readonly any[], TShape extends readonly any[]> = And<
[
// both arrays
IsArray<TValue>,
IsArray<TShape>,
// same length
SameLength<TValue, TShape>,
// both readonly or not
Xor<IsReadonly<TValue>, IsReadonly<TShape>>,
]
> extends true
? [TValue, TShape] extends [readonly (infer TValueElement)[], readonly (infer TShapeElement)[]]
? Exact<TValueElement, TShapeElement> extends TValueElement
? TValue
: never
: never
: never;

type PrimitiveExact<TValue, TShape> = [TValue] extends [TShape] ? ([TShape] extends [TValue] ? TValue : never) : never;

export type Exact<TValue, TShape> = [TValue] extends [readonly any[]]
? [TShape] extends [readonly any[]]
? ArrayExact<TValue, TShape>
: never
: [TValue] extends [AnyRecord]
? ObjectExact<TValue, TShape>
: PrimitiveExact<TValue, TShape>;
7 changes: 3 additions & 4 deletions lib/functions/is-exact/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Exact } from "../../exact";

export const isExact =
<Expected>() =>
<Actual>(actual: Exact<Actual, Expected>): Expected => {
return actual;
};
<ExpectedShape>() =>
<ActualShape>(x: Exact<ActualShape, ExpectedShape>) =>
x as ExpectedShape;
168 changes: 166 additions & 2 deletions test/is-exact.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,50 @@
import { isExact } from "../lib";

function testIsExact() {
function testArray() {
const readonlyArray: readonly number[] = [1, 2, 3];
const writableArray: number[] = [1, 2, 3];
const tuple: [number] = [1];
const readonlyTuple = [1, 2, 3] as const;

const isReadonlyArray = isExact<readonly number[]>();
const isWritableArray = isExact<number[]>();
const isTuple = isExact<[number]>();
const isReadonlyTuple = isExact<readonly [1, 2, 3]>();

isReadonlyArray(readonlyArray);
// @ts-expect-error: doesn't have `readonly`
isReadonlyArray(writableArray);
// @ts-expect-error: doesn't have `readonly` and is tuple
isReadonlyArray(tuple);
// @ts-expect-error: is tuple
isReadonlyArray(readonlyTuple);

// @ts-expect-error: has `readonly`
isWritableArray(readonlyArray);
isWritableArray(writableArray);
// @ts-expect-error: is tuple
isWritableArray(tuple);
// @ts-expect-error: has `readonly` and is tuple
isWritableArray(readonlyTuple);

// @ts-expect-error: has `readonly` and isn't tuple
isTuple(readonlyArray);
// @ts-expect-error: isn't tuple
isTuple(writableArray);
isTuple(tuple);
// @ts-expect-error: has `readonly`
isTuple(readonlyTuple);

// @ts-expect-error: isn't tuple
isReadonlyTuple(readonlyArray);
// @ts-expect-error: has NO `readonly` and isn't tuple
isReadonlyTuple(writableArray);
// @ts-expect-error: has NO `readonly`
isReadonlyTuple(tuple);
isReadonlyTuple(readonlyTuple);
}

function testObjects() {
type ABC = { a: number; b: number; c: number };
type BC = { b: number; c: number };
type BC2 = { b: number; c: string };
Expand All @@ -26,7 +70,7 @@ function testIsExact() {
isBC(bc);
// @ts-expect-error has different structure from BC (c has different type)
isBC(bc2);
// has the same structure as BC
// @ts-expect-error: has different structure from BC (b and c have different types)
isBC(bc3);
// @ts-expect-error has different structure from BC (c has different type)
isBC(bc4);
Expand All @@ -36,3 +80,123 @@ function testIsExact() {
// @ts-expect-error has different structure from BC (missing property b)
isBC(c2);
}

function testObjectUnionType() {
type ABC = { a: number; b: number; c: number };
type BC = { b: number; c: number };

let abcOrBc: ABC | BC = { a: 1, b: 2, c: 3 };
let bc3 = { b: 2, c: 3 } as const;
let bcOrBc3: BC | typeof bc3 = bc3;

const isBC = isExact<BC>();

// @ts-expect-error has different structure from BC (excessive `ABC` union element)
isBC(abcOrBc);

// @ts-expect-error: has different structure from BC (b and c have different type)
isBC(bcOrBc3);

const isBCorBC3 = isExact<BC | typeof bc3>();

// has the same structure
isBCorBC3(bcOrBc3);
}

function testObjectUndefinedUnionProperties() {
type RequiredA = { a: number };
type OptionalA = { a: number | undefined };

const requiredA: RequiredA = { a: 1 };
const optionalA: OptionalA = { a: 1 };

const isRequiredA = isExact<RequiredA>();
const isOptionalA = isExact<OptionalA>();

// as the same structure as RequiredA
isRequiredA(requiredA);
// @ts-expect-error has different structure from BC (a has excessive `undefined` union element)
isRequiredA(optionalA);
// @ts-expect-error has different structure from BC (a has missed `undefined` union element)
isOptionalA(requiredA);
// as the same structure as OptionalA
isOptionalA(optionalA);
}

function testPrimitiveUnionType() {
type MaybeNumber = number | undefined;

const numericLiteral = 10;
let numericLiteral2 = 10 as 10;
const number = 10 as number;
let number2 = 10;
let number3: number | undefined = 10;
const maybeNumber = 10 as MaybeNumber;
const maybeNumber2 = 10 as number | undefined;
let maybeNumber3 = Math.random() > 0.5 ? 10 : undefined;

const isNumber = isExact<number>();
const isMaybeNumber = isExact<MaybeNumber>();
const isMaybeNumber2 = isExact<number | undefined>();

// @ts-expect-error has different type from number (numeric literal type)
isNumber(numericLiteral);
// @ts-expect-error has different type from number (numeric literal type)
isNumber(numericLiteral2);
isNumber(number);
isNumber(number2);
isNumber(number3);
// @ts-expect-error has different type from number (excessive `undefined` union element)
isNumber(maybeNumber);
// @ts-expect-error has different type from number (excessive `undefined` union element)
isNumber(maybeNumber2);
// @ts-expect-error has different type from number (excessive `undefined` union element)
isNumber(maybeNumber3);

// @ts-expect-error has different type from MaybeNumber (numeric literal type)
isMaybeNumber(numericLiteral);
// @ts-expect-error has different type from MaybeNumber (numeric literal type)
isMaybeNumber(numericLiteral2);
isMaybeNumber(maybeNumber);
isMaybeNumber(maybeNumber2);
isMaybeNumber(maybeNumber3);
// @ts-expect-error has different type from MaybeNumber (missing `undefined` union element)
isMaybeNumber(number);
// @ts-expect-error has different type from MaybeNumber (missing `undefined` union element)
isMaybeNumber(number2);
// @ts-expect-error has different type from MaybeNumber (missing `undefined` union element)
isMaybeNumber(number3);

// @ts-expect-error has different type from MaybeNumber (numeric literal type)
isMaybeNumber2(numericLiteral);
// @ts-expect-error has different type from MaybeNumber (numeric literal type)
isMaybeNumber2(numericLiteral2);
isMaybeNumber2(maybeNumber);
isMaybeNumber2(maybeNumber2);
isMaybeNumber2(maybeNumber3);
// @ts-expect-error has different type from MaybeNumber (missing `undefined` union element)
isMaybeNumber2(number);
// @ts-expect-error has different type from MaybeNumber (missing `undefined` union element)
isMaybeNumber2(number2);
// @ts-expect-error has different type from MaybeNumber (missing `undefined` union element)
isMaybeNumber2(number3);
}

function testEnums() {
enum SingleValueEnum {
Foo = "foo",
}
enum MultipleValueEnum {
Foo = "foo",
Bar = "bar",
Baz = "baz",
}

const singleValueEnum = SingleValueEnum.Foo;
const multipleValueEnum = MultipleValueEnum.Foo;

isExact<SingleValueEnum>()(singleValueEnum);
// TODO: fix under a separate bug
// @ts-expect-error: Argument of type 'MultipleEnum' is not assignable to parameter of type 'never'
isExact<MultipleValueEnum>()(multipleValueEnum);
}

0 comments on commit 25f3f60

Please sign in to comment.