Skip to content

Commit

Permalink
fix: Assignability for Mark* utility types (#364)
Browse files Browse the repository at this point in the history
* test: βž• add Debug

* test: βž• add assignability checks

* fix: πŸ§ͺ MarkRequired

* fix: πŸ§ͺ MarkReadonly

* test: πŸ§ͺ testMarkWritable

* fix: πŸ§ͺ MarkWritable

* ci: ❌ remove TS@<4.5 support

* feat: βž• add Prettify

* docs: πŸ“„ Prettify

* test: ❌ remove edge cases for TS@<4.5

* refactor: πŸ”„ Debug => Prettify

* docs: πŸ“„ changeset
  • Loading branch information
Beraliv committed Apr 28, 2024
1 parent de04efa commit 26be790
Show file tree
Hide file tree
Showing 19 changed files with 257 additions and 167 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-phones-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ts-essentials": major
---

Fixed assignability of Mark\* utility types which required removing support of TypeScript@<4.5
17 changes: 1 addition & 16 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,7 @@ jobs:
matrix:
node: ["14.x"]
os: [ubuntu-latest]
typescript:
[
"4.2.4",
"4.3.5",
"4.4.4",
"4.5.5",
"4.6.4",
"4.7.4",
"4.8.4",
"4.9.4",
"5.0.4",
"5.1.6",
"5.2.2",
"5.3.3",
"5.4.3",
]
typescript: ["4.5.5", "4.6.4", "4.7.4", "4.8.4", "4.9.4", "5.0.4", "5.1.6", "5.2.2", "5.3.3", "5.4.3"]
runs-on: ${{ matrix.os }}

steps:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ npm install --save-dev ts-essentials
- [`Builtin`](/lib/built-in) - Matches primitive, function, date, error or regular expression
- [`KeyofBase`](/lib/key-of-base) -
[`keyofStringsOnly`](https://www.typescriptlang.org/tsconfig#keyofStringsOnly)-tolerant analogue for `PropertyKey`
- [`Prettify<Type>`](/lib/prettify/) - flattens type and makes it more readable on the hover in your IDE
- [`Primitive`](/lib/primitive) - Matches any
[primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive)
- [`StrictExclude<UnionType, ExcludedMembers>`](/lib/strict-exclude) - Constructs a type by excluding from `UnionType`
Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from "./opaque";
export * from "./path-value";
export * from "./paths";
export * from "./pick-properties";
export * from "./prettify";
export * from "./safe-dictionary";
export * from "./union-to-intersection";
export * from "./value-of";
Expand Down
6 changes: 5 additions & 1 deletion lib/mark-readonly/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { ReadonlyKeys } from "../readonly-keys";
import { Writable } from "../writable";

export type MarkReadonly<Type, Keys extends keyof Type> = Type extends Type
? Omit<Type, Keys> & Readonly<Pick<Type, Keys>>
? Readonly<Type> &
Writable<Pick<Type, Exclude<keyof Type, Keys | (Type extends object ? ReadonlyKeys<Type> : never)>>>
: never;
4 changes: 1 addition & 3 deletions lib/mark-required/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export type MarkRequired<Type, Keys extends keyof Type> = Type extends Type
? Omit<Type, Keys> & Required<Pick<Type, Keys>>
: never;
export type MarkRequired<Type, Keys extends keyof Type> = Type extends Type ? Type & Required<Pick<Type, Keys>> : never;
3 changes: 2 additions & 1 deletion lib/mark-writable/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Writable } from "../writable";
import { WritableKeys } from "../writable-keys";

export type MarkWritable<Type, Keys extends keyof Type> = Type extends Type
? Omit<Type, Keys> & Writable<Pick<Type, Keys>>
? Readonly<Type> & Writable<Pick<Type, (Type extends object ? WritableKeys<Type> : never) | Keys>>
: never;
32 changes: 32 additions & 0 deletions lib/prettify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
`Prettify<Type>` flattens type and makes it more readable on the hover in your IDE.

It shows what properties are included in interfaces:

```ts
interface Name {
first: string;
second: string;
}

type NameOnly = Name;
// ^? Name

type FullName = Prettify<Name>;
// ^? {first: string; second: string}
```

It flattens intersections:

```ts
type Intersection = Name & {
address: string;
};

type IntersectionOnly = Intersection;
// ^? Name & {address: string}

type EverythingAboutPerson = Prettify<Intersection>;
// ^? {first: string; second: string; address: string}
```

TS Playground - https://tsplay.dev/m3d51W
3 changes: 3 additions & 0 deletions lib/prettify/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type Prettify<Type> = {
[Key in keyof Type]: Type[Key];
} & {};
4 changes: 1 addition & 3 deletions lib/xor/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
type Prettify<Type> = {
[Key in keyof Type]: Type[Key];
} & {};
import { Prettify } from "../prettify";

type Without<Type1, Type2> = { [P in Exclude<keyof Type1, keyof Type2>]?: never };

Expand Down
21 changes: 4 additions & 17 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ function testDictionary() {
Assert<IsExact<Dictionary<number, "a" | "b">[string], number>>,
Assert<IsExact<Dictionary<number, "a" | "b">["a"], number>>,
Assert<IsExact<Dictionary<number, "a" | "b">["b"], number>>,
// for TypeScript 4.2 and 4.3 it doesn't work, so using `string` to make it work on purpose
Assert<IsExact<Dictionary<number, KeyofBase>[TsVersion extends "4.2" | "4.3" ? string : symbol], number>>,
Assert<IsExact<Dictionary<number, KeyofBase>[symbol], number>>,
];
}

Expand Down Expand Up @@ -516,14 +515,7 @@ function testOptionalKeys() {
// @ts-expect-error converts to BigInt and gets its optional keys
Assert<IsExact<OptionalKeys<bigint>, never>>,
// wtf?
Assert<
IsExact<
OptionalKeys<symbol>,
TsVersion extends "4.2"
? (() => string) | (() => symbol)
: string | ((hint: string) => symbol) | (() => string) | (() => symbol)
>
>,
Assert<IsExact<OptionalKeys<symbol>, string | ((hint: string) => symbol) | (() => string) | (() => symbol)>>,
Assert<IsExact<OptionalKeys<undefined>, never>>,
Assert<IsExact<OptionalKeys<null>, never>>,
Assert<IsExact<OptionalKeys<Function>, never>>,
Expand All @@ -549,15 +541,10 @@ function testOptionalKeys() {
function testRequiredKeys() {
type cases = [
Assert<IsExact<RequiredKeys<number>, keyof Number>>,
Assert<IsExact<RequiredKeys<string>, TsVersion extends "4.2" ? never : SymbolConstructor["iterator"]>>,
Assert<IsExact<RequiredKeys<string>, SymbolConstructor["iterator"]>>,
Assert<IsExact<RequiredKeys<boolean>, keyof Boolean>>,
Assert<IsExact<RequiredKeys<bigint>, keyof BigInt>>,
Assert<
IsExact<
RequiredKeys<symbol>,
TsVersion extends "4.2" ? "toString" | "valueOf" : typeof Symbol.toPrimitive | typeof Symbol.toStringTag
>
>,
Assert<IsExact<RequiredKeys<symbol>, typeof Symbol.toPrimitive | typeof Symbol.toStringTag>>,
Assert<IsExact<RequiredKeys<undefined>, never>>,
Assert<IsExact<RequiredKeys<null>, never>>,
Assert<IsExact<RequiredKeys<Function>, keyof Function>>,
Expand Down
74 changes: 51 additions & 23 deletions test/mark-optional.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
import { AssertTrue as Assert, IsExact } from "conditional-type-checks";
import { MarkOptional, OptionalKeys, RequiredKeys } from "../lib";
import { MarkOptional, OptionalKeys, Prettify, RequiredKeys } from "../lib";

function testMarkOptional() {
type Example = {
required1: number;
required2: string;
optional1?: null;
optional2?: boolean;
};

type UnionExample = MarkOptional<
Pick<Example, "required1" | "optional1"> | Pick<Example, "required2" | "optional1">,
"optional1"
>;

let unionElementFields: UnionExample = {
required1: 1,
optional1: null,
};

unionElementFields = {
required2: "2",
optional1: null,
};
type Example = {
required1: number;
required2: string;
optional1?: null;
optional2?: boolean;
};

function testMarkOptional() {
type cases = [
Assert<IsExact<MarkOptional<Example, never>, Example>>,
Assert<IsExact<MarkOptional<Example, OptionalKeys<Example>>, Example>>,
Expand All @@ -43,3 +28,46 @@ function testMarkOptional() {
MarkOptional<Example | { a: 1 }, "required1">,
];
}

function testUnionTypes() {
type UnionExample = Prettify<
MarkOptional<Pick<Example, "required1" | "optional1"> | Pick<Example, "required2" | "optional1">, "optional1">
>;

let unionElementFields: UnionExample = {
required1: 1,
optional1: null,
};

unionElementFields = {
required2: "2",
optional1: null,
};
}

declare let example: Example;
declare let optionalExample: Partial<Example>;
declare let markedOptionalExample: Prettify<MarkOptional<Example, "required1">>;

function testAssignability() {
// @ts-expect-error: Type 'Partial<Example>' is not assignable to type 'Example'
example = optionalExample;
// @ts-expect-error: Type 'Omit<Example, "required1"> & Partial<Pick<Example, "required1">>' is not assignable to type 'Example'
example = markedOptionalExample;
optionalExample = example;
markedOptionalExample = example;

// it verifies that type `Partial<Type>` is NOT assignable to type `Type`

// @ts-expect-error: Type 'Partial<Type>' is not assignable to type 'Type'
let assignabilityCheck1: <Type>(object: Type) => object is Partial<Type>;

// it verifies that type `MarkOptional<Type, PropertyName>`
// is NOT assignable to type `Type`

let assignabilityCheck2: <Type, PropertyName extends keyof Type>(
object: Type,
propertyNames: PropertyName[],
// @ts-expect-error: Type 'MarkOptional<Type, PropertyName>' is not assignable to type 'Type'
) => object is MarkOptional<Type, PropertyName>;
}
78 changes: 46 additions & 32 deletions test/mark-readonly.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { AssertTrue as Assert, IsExact } from "conditional-type-checks";
import { MarkReadonly, WritableKeys, ReadonlyKeys } from "../lib";
import { MarkReadonly, WritableKeys, ReadonlyKeys, Prettify } from "../lib";

type Example = {
readonly readonly1: Date;
readonly readonly2: RegExp;
required1: number;
required2: string;
optional1?: null;
optional2?: boolean;
};

function testMarkReadonly() {
type Example = {
readonly readonly1: Date;
readonly readonly2: RegExp;
required1: number;
required2: string;
optional1?: null;
optional2?: boolean;
};
type ExampleWithReadonlyRequired1 = Prettify<MarkReadonly<Example, "required1">>;

type cases = [
Assert<IsExact<MarkReadonly<Example, never>, Example>>,
Assert<IsExact<MarkReadonly<Example, ReadonlyKeys<Example>>, Example>>,
Assert<IsExact<MarkReadonly<Example, WritableKeys<Example>>, Readonly<Example>>>,
Assert<IsExact<ReadonlyKeys<ExampleWithReadonlyRequired1>, "readonly1" | "readonly2" | "required1">>,
// @ts-expect-error do NOT support union types
MarkReadonly<Example | { a: 1 }, "required1">,
];
}

type UnionExample = MarkReadonly<
Pick<Example, "readonly1" | "optional1"> | Pick<Example, "readonly2" | "optional1">,
"optional1"
function testUnionTypes() {
type UnionExample = Prettify<
MarkReadonly<Pick<Example, "readonly1" | "optional1"> | Pick<Example, "readonly2" | "optional1">, "optional1">
>;

let unionElementFields: UnionExample = {
Expand All @@ -25,25 +37,27 @@ function testMarkReadonly() {
readonly1: new Date(),
optional1: null,
};
}

type cases = [
Assert<IsExact<MarkReadonly<Example, never>, Example>>,
Assert<IsExact<MarkReadonly<Example, ReadonlyKeys<Example>>, Example>>,
Assert<IsExact<MarkReadonly<Example, WritableKeys<Example>>, Readonly<Example>>>,
Assert<
IsExact<
MarkReadonly<Example, "required1">,
{
readonly readonly1: Date;
readonly readonly2: RegExp;
readonly required1: number;
required2: string;
optional1?: null;
optional2?: boolean;
}
>
>,
// @ts-expect-error do NOT support union types
MarkReadonly<Example | { a: 1 }, "required1">,
];
declare let example: Example;
declare let readonlyExample: Readonly<Example>;
declare let markedReadonlyExample: MarkReadonly<Example, "optional1">;

function testAssignability() {
example = readonlyExample;
example = markedReadonlyExample;
readonlyExample = example;
markedReadonlyExample = example;

// it verifies that type `Readonly<Type>` is assignable to type `Type`

let assignabilityCheck1: <Type>(object: Type) => object is Readonly<Type>;

// it verifies that type `MarkReadonly<Type, PropertyName>`
// is assignable to type `Type`

let assignabilityCheck2: <Type, PropertyName extends keyof Type>(
object: Type,
propertyNames: PropertyName[],
) => object is MarkReadonly<Type, PropertyName>;
}

0 comments on commit 26be790

Please sign in to comment.