From 25f3f60abb6f32977f8f0e8068321b53e43fafc7 Mon Sep 17 00:00:00 2001 From: Alexey Berezin <2991847+Beraliv@users.noreply.github.com> Date: Sun, 28 Apr 2024 01:10:59 +0100 Subject: [PATCH] fix: Add support of union types for arrays, tuples, objects and primitive in isExact (#345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .changeset/swift-eels-obey.md | 5 + lib/exact/index.ts | 95 +++++++++++++++++- lib/functions/is-exact/index.ts | 7 +- test/is-exact.ts | 168 +++++++++++++++++++++++++++++++- 4 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 .changeset/swift-eels-obey.md diff --git a/.changeset/swift-eels-obey.md b/.changeset/swift-eels-obey.md new file mode 100644 index 00000000..f751e5e7 --- /dev/null +++ b/.changeset/swift-eels-obey.md @@ -0,0 +1,5 @@ +--- +"ts-essentials": patch +--- + +Add support of union types for arrays, tuples, objects and primitive in `isExact` diff --git a/lib/exact/index.ts b/lib/exact/index.ts index 598717ad..2e203a64 100644 --- a/lib/exact/index.ts +++ b/lib/exact/index.ts @@ -1,5 +1,94 @@ -export type Exact = Type extends Shape - ? Exclude extends never - ? Type +import { AnyRecord } from "../any-record"; +import { IsNever } from "../is-never"; + +type IsUnion = UnionToTuple["length"] extends 1 ? false : true; + +type UnionToFunctionInsertion = (TUnion extends any ? (arg: () => TUnion) => any : never) extends ( + arg: infer TParam, +) => any + ? TParam + : never; + +type UnionToTuple = UnionToFunctionInsertion extends () => infer TReturnType + ? [...UnionToTuple>, TReturnType] + : []; + +type ExactUnionLength< + TValue, + TShape, + TValueLength = UnionToTuple["length"], + TShapeLength = UnionToTuple["length"], +> = TValueLength extends TShapeLength ? true : false; + +type Xor = T extends true ? (U extends true ? true : false) : U extends false ? true : false; + +type And = TTuple extends [infer Head, ...infer Rest] + ? Head extends true + ? And + : false + : TTuple extends [] + ? true + : false; + +type ObjectKeyExact = And< + [IsNever>, IsNever>] +>; + +type ObjectValueDiff = { + [TKey in keyof TValue]: Exclude; +}[keyof TValue]; + +type ObjectValueExact = And< + [IsNever>, IsNever>] +>; + +type ObjectExact = [TValue] extends [TShape] + ? And< + [ + Xor, IsUnion>, + ExactUnionLength, + ObjectKeyExact, + ObjectValueExact, + ] + > extends true + ? TValue + : never + : never; + +type IsArray = [TValue] extends [readonly any[]] ? true : false; + +type IsReadonly = Readonly extends TArray ? true : false; + +type SameLength = IsNever< + PrimitiveExact +> extends true + ? false + : true; + +type ArrayExact = And< + [ + // both arrays + IsArray, + IsArray, + // same length + SameLength, + // both readonly or not + Xor, IsReadonly>, + ] +> extends true + ? [TValue, TShape] extends [readonly (infer TValueElement)[], readonly (infer TShapeElement)[]] + ? Exact extends TValueElement + ? TValue + : never : never : never; + +type PrimitiveExact = [TValue] extends [TShape] ? ([TShape] extends [TValue] ? TValue : never) : never; + +export type Exact = [TValue] extends [readonly any[]] + ? [TShape] extends [readonly any[]] + ? ArrayExact + : never + : [TValue] extends [AnyRecord] + ? ObjectExact + : PrimitiveExact; diff --git a/lib/functions/is-exact/index.ts b/lib/functions/is-exact/index.ts index 5175982d..e45e2089 100644 --- a/lib/functions/is-exact/index.ts +++ b/lib/functions/is-exact/index.ts @@ -1,7 +1,6 @@ import { Exact } from "../../exact"; export const isExact = - () => - (actual: Exact): Expected => { - return actual; - }; + () => + (x: Exact) => + x as ExpectedShape; diff --git a/test/is-exact.ts b/test/is-exact.ts index 98a7629b..ac2aed1e 100644 --- a/test/is-exact.ts +++ b/test/is-exact.ts @@ -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(); + const isWritableArray = isExact(); + const isTuple = isExact<[number]>(); + const isReadonlyTuple = isExact(); + + 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 }; @@ -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); @@ -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(); + + // @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(); + + // 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(); + const isOptionalA = isExact(); + + // 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(); + const isMaybeNumber = isExact(); + const isMaybeNumber2 = isExact(); + + // @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); + // TODO: fix under a separate bug + // @ts-expect-error: Argument of type 'MultipleEnum' is not assignable to parameter of type 'never' + isExact()(multipleValueEnum); +}