Skip to content

Commit

Permalink
KeysOfUnion: Fix assignability with keyof (#1009)
Browse files Browse the repository at this point in the history
  • Loading branch information
som-sm authored Jan 20, 2025
1 parent e7800af commit 4789c7c
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 7 deletions.
8 changes: 5 additions & 3 deletions source/keys-of-union.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type {UnionToIntersection} from './union-to-intersection';

/**
Create a union of all keys from a given type, even those exclusive to specific union members.
Expand Down Expand Up @@ -35,6 +37,6 @@ type AllKeys = KeysOfUnion<Union>;
@category Object
*/
export type KeysOfUnion<ObjectType> = ObjectType extends unknown
? keyof ObjectType
: never;
export type KeysOfUnion<ObjectType> =
// Hack to fix https://github.com/sindresorhus/type-fest/issues/1008
keyof UnionToIntersection<ObjectType extends unknown ? Record<keyof ObjectType, never> : never>;
2 changes: 1 addition & 1 deletion source/set-optional.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export type SetOptional<BaseType, Keys extends keyof BaseType> =
// Pick just the keys that are readonly from the base type.
Except<BaseType, Keys> &
// Pick the keys that should be mutable from the base type and make them mutable.
Partial<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
Partial<HomomorphicPick<BaseType, Keys>>
>
: never;
2 changes: 1 addition & 1 deletion source/set-readonly.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ export type SetReadonly<BaseType, Keys extends keyof BaseType> =
BaseType extends unknown
? Simplify<
Except<BaseType, Keys> &
Readonly<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
Readonly<HomomorphicPick<BaseType, Keys>>
>
: never;
2 changes: 1 addition & 1 deletion source/set-required.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type SetRequired<BaseType, Keys extends keyof BaseType> =
// Pick just the keys that are optional from the base type.
Except<BaseType, Keys> &
// Pick the keys that should be required from the base type and make them required.
Required<HomomorphicPick<BaseType, Keys & KeysOfUnion<BaseType>>>
Required<HomomorphicPick<BaseType, Keys>>
>;

/**
Expand Down
51 changes: 50 additions & 1 deletion test-d/keys-of-union.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {expectType} from 'tsd';
import type {KeysOfUnion} from '../index';
import type {KeysOfUnion, UnknownRecord} from '../index';

// When passing types that are not unions, it behaves exactly as the `keyof` operator.

Expand Down Expand Up @@ -35,3 +35,52 @@ type Expected2 = 'common' | 'a' | 'b' | 'c';
declare const actual2: KeysOfUnion<Example2>;

expectType<Expected2>(actual2);

// With property modifiers
declare const actual3: KeysOfUnion<{a?: string; readonly b: number} | {a: number; b: string}>;
expectType<'a' | 'b'>(actual3);

// `KeysOfUnion<T>` should NOT be assignable to `keyof T`
type Assignability1<T, _K extends keyof T> = unknown;
// @ts-expect-error
type Test1<T> = Assignability1<T, KeysOfUnion<T>>;

// `keyof T` should be assignable to `KeysOfUnion<T>`
type Assignability2<T, _K extends KeysOfUnion<T>> = unknown;
type Test2<T> = Assignability2<T, keyof T>;

// `KeysOfUnion<T>` should be assignable to `PropertyKey`
type Assignability3<_T, _K extends PropertyKey> = unknown;
type Test3<T> = Assignability3<T, KeysOfUnion<T>>;

// `PropertyKey` should NOT be assignable to `KeysOfUnion<T>`
type Assignability4<T, _K extends KeysOfUnion<T>> = unknown;
// @ts-expect-error
type Test4<T> = Assignability4<T, PropertyKey>;

// `keyof T` should be assignable to `KeysOfUnion<T>` even when `T` is constrained to `Record<string, unknown>`
type Assignability5<T extends Record<string, unknown>, _K extends KeysOfUnion<T>> = unknown;
type Test5<T extends Record<string, unknown>> = Assignability5<T, keyof T>;

// `keyof T` should be assignable to `KeysOfUnion<T>` even when `T` is constrained to `object`
type Assignability6<T extends object, _K extends KeysOfUnion<T>> = unknown;
type Test6<T extends object> = Assignability6<T, keyof T>;

// `keyof T` should be assignable to `KeysOfUnion<T>` even when `T` is constrained to `UnknownRecord`
type Assignability7<T extends UnknownRecord, _K extends KeysOfUnion<T>> = unknown;
type Test7<T extends UnknownRecord> = Assignability7<T, keyof T>;

// `KeysOfUnion<T>` should NOT be assignable to `keyof T` even when `T` is constrained to `Record<string, unknown>`
type Assignability8<T extends Record<string, unknown>, _K extends keyof T> = unknown;
// @ts-expect-error
type Test8<T extends Record<string, unknown>> = Assignability8<T, KeysOfUnion<T>>;

// `KeysOfUnion<T>` should NOT be assignable to `keyof T` even when `T` is constrained to `object`
type Assignability9<T extends object, _K extends keyof T> = unknown;
// @ts-expect-error
type Test9<T extends object> = Assignability9<T, KeysOfUnion<T>>;

// `KeysOfUnion<T>` should NOT be assignable to `keyof T` even when `T` is constrained to `UnknownRecord`
// type Assignability10<T extends UnknownRecord, _K extends keyof T> = unknown;
// The following line should error but it doesn't, this is an issue with the existing implementation of `KeysOfUnion`
// type Test10<T extends UnknownRecord> = Assignability10<T, KeysOfUnion<T>>;

0 comments on commit 4789c7c

Please sign in to comment.