Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d001faa
feat: add `RenameKey` type
materwelonDhruv May 28, 2026
80c9aec
feat: add `RenameKeys` type
materwelonDhruv May 28, 2026
0eddd05
fix(lint): lint error in import statement
materwelonDhruv May 28, 2026
229c206
fix: reject key collisions and optional map entries in `RenameKey`/`R…
materwelonDhruv May 29, 2026
9c3e2d3
tests: cover `RenameKey`/`RenameKeys` edge cases and rejections
materwelonDhruv May 29, 2026
889d438
fix: linter needs enable statements after file-level disable statements
materwelonDhruv May 29, 2026
a407f21
fix: various issues
materwelonDhruv May 31, 2026
92b4a86
chore: update readme
materwelonDhruv May 31, 2026
6bf4632
fix: lint issues
materwelonDhruv May 31, 2026
d556969
feat: add reliable merging behavior
materwelonDhruv Jun 22, 2026
c90f79b
chore: remove `RenameKey` type
materwelonDhruv Jun 22, 2026
4f71ea3
refactor: use the existing internal type for this
materwelonDhruv Jun 22, 2026
c4a9851
refactor: make option keys no-op instead of fail
materwelonDhruv Jun 22, 2026
8da94ac
refactor: allow unions distributing their types on rename
materwelonDhruv Jun 22, 2026
c09e705
fix: correct modifier handling on merged targets
materwelonDhruv Jun 23, 2026
bbba36a
fix: unsynced readme desc
materwelonDhruv Jun 23, 2026
231568a
fix: incorrect optional behavior with EOPT off
materwelonDhruv Jun 26, 2026
3b6b979
fix: make non-literal targets no op instead of produce a `never`
materwelonDhruv Jun 26, 2026
438f46f
refactor: simplify the type's impl + some more tests
materwelonDhruv Jul 9, 2026
8615b9b
Merge remote-tracking branch 'upstream/main' into add-rename-key
materwelonDhruv Jul 9, 2026
aafd4ea
fix: updated API
materwelonDhruv Jul 9, 2026
2b3db9a
refactor: the source value type is not needed anymore
materwelonDhruv Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export type {KeyAsString} from './source/key-as-string.d.ts';
export type {Exact} from './source/exact.d.ts';
export type {ReadonlyTuple} from './source/readonly-tuple.d.ts';
export type {OverrideProperties} from './source/override-properties.d.ts';
export type {RenameKey} from './source/rename-key.d.ts';
export type {RenameKeys} from './source/rename-keys.d.ts';
export type {OptionalKeysOf} from './source/optional-keys-of.d.ts';
export type {IsOptionalKeyOf} from './source/is-optional-key-of.d.ts';
export type {HasOptionalKeys} from './source/has-optional-keys.d.ts';
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Click the type names for complete docs.
- [`MergeDeep`](source/merge-deep.d.ts) - Merge two objects or two arrays/tuples recursively into a new type.
- [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive keys.
- [`OverrideProperties`](source/override-properties.d.ts) - Override existing properties of the given type. Similar to `Merge`, but enforces that the original type has the properties you want to override.
- [`RenameKey`](source/rename-key.d.ts) - Rename a single key in an object type, preserving its value type and modifiers (optional, readonly).
- [`RenameKeys`](source/rename-keys.d.ts) - Rename multiple keys in an object type at once, preserving each value type and modifiers (optional, readonly).
- [`RequireAtLeastOne`](source/require-at-least-one.d.ts) - Create a type that requires at least one of the given keys, while keeping the remaining keys as is.
- [`RequireExactlyOne`](source/require-exactly-one.d.ts) - Create a type that requires exactly one of the given keys and disallows more, while keeping the remaining keys as is.
- [`RequireAllOrNone`](source/require-all-or-none.d.ts) - Create a type that requires all of the given keys or none of the given keys, while keeping the remaining keys as is.
Expand Down
80 changes: 80 additions & 0 deletions source/rename-key.d.ts
Comment thread
materwelonDhruv marked this conversation as resolved.
Outdated
Comment thread
materwelonDhruv marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type {KeysOfUnion} from './keys-of-union.d.ts';

/**
Rename a single key in an object type, preserving its value type and modifiers (optional, readonly).

Useful when you want to derive a new type from an existing one with one key relabeled, without losing the original value type or remapping every property. See {@link RenameKeys} for the multi-key variant.

Renaming to a key that already exists is rejected, because collapsing two keys into one would also collapse their modifiers (an optional source key could silently make a required target key optional). Renaming a key to itself is allowed and is a no-op.

@example
```
import type {RenameKey} from 'type-fest';

type User = {
id: string;
name: string;
createdAt: Date;
};

type Renamed = RenameKey<User, 'createdAt', 'created_at'>;
//=> {id: string; name: string; created_at: Date}
```

Works with unions, distributing over each member:

@example
```
import type {RenameKey} from 'type-fest';

type Event =
| {kind: 'click'; target: string}
| {kind: 'submit'; target: HTMLFormElement};

type Renamed = RenameKey<Event, 'target', 'element'>;
//=> {
// kind: 'click';
// element: string;
// } | {
// kind: 'submit';
// element: HTMLFormElement;
// }
```

Preserves optional and readonly modifiers:

@example
```
import type {RenameKey} from 'type-fest';

type Source = {
readonly id: string;
label?: string;
};

type Renamed = RenameKey<RenameKey<Source, 'id', 'identifier'>, 'label', 'name'>;
//=> {readonly identifier: string; name?: string}
```

Renaming onto an existing key is rejected:

@example
```
import type {RenameKey} from 'type-fest';

// @ts-expect-error 'b' already exists on the source.
type Bad = RenameKey<{a: number; b: string}, 'a', 'b'>;
// Error: Type '"b"' does not satisfy the constraint 'never'.
```

@category Object
*/
export type RenameKey<
BaseType,
FromKey extends KeysOfUnion<BaseType>,
ToKey extends [ToKey] extends [Exclude<KeysOfUnion<BaseType>, FromKey>] ? never : PropertyKey,
> = {
[Key in keyof BaseType as Key extends FromKey ? ToKey : Key]: BaseType[Key];
};

export {};
59 changes: 59 additions & 0 deletions source/rename-keys.d.ts
Comment thread
materwelonDhruv marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type {KeysOfUnion} from './keys-of-union.d.ts';

/**
Rename multiple keys in an object type at once, preserving each value type and modifiers (optional, readonly).

Each entry in the rename map relabels one source key to one target key. Keys absent from the map are kept unchanged. See {@link RenameKey} for the single-key variant.

Map entries must be required and their targets must not collide: an entry whose target already exists as a kept key, or two entries sharing a target, are rejected, because collapsing keys would also collapse their modifiers. Swapping keys (each renamed away to the other's name) is allowed since no key is lost.

@example
```
import type {RenameKeys} from 'type-fest';

type User = {
id: string;
firstName: string;
createdAt: Date;
};

type Renamed = RenameKeys<User, {firstName: 'first_name'; createdAt: 'created_at'}>;
//=> {id: string; first_name: string; created_at: Date}
```

The rename map constraint enforces that every source key exists on the input so you can't mess up the key names and you also get intellisense:

@example
```
import type {RenameKeys} from 'type-fest';

type User = {id: string; name: string};

// @ts-expect-error 'nme' is not a key of User.
type Bad = RenameKeys<User, {nme: 'fullName'}>;
```

@category Object
*/
export type RenameKeys<
BaseType,
RenameMap extends Partial<Record<KeysOfUnion<BaseType>, PropertyKey>> & {
[Key in keyof RenameMap]: Key extends KeysOfUnion<BaseType>
? RenameMap[Key] extends PropertyKey
? RenameMap[Key] extends Exclude<KeysOfUnion<BaseType>, keyof RenameMap>
? never
: RenameMap[Key] extends RenameMap[Exclude<keyof RenameMap, Key>]
? never
: RenameMap[Key]
: never
: never;
},
Comment thread
materwelonDhruv marked this conversation as resolved.
Outdated
> = {
[Key in keyof BaseType as Key extends keyof RenameMap
? RenameMap[Key] extends PropertyKey
? RenameMap[Key]
: Key
: Key]: BaseType[Key];
};

export {};
178 changes: 178 additions & 0 deletions test-d/rename-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {expectType} from 'tsd';
import {expectTypeOf} from 'expect-type';
import type {RenameKey} from '../source/rename-key.d.ts';

/* eslint-disable no-lone-blocks -- Each case is wrapped in a block to scope its reused local aliases (`Source`, `Bad`). */

// Basic rename.
{
type User = {
id: string;
name: string;
createdAt: Date;
};

const fixture: RenameKey<User, 'createdAt', 'created_at'> = {
id: 'a',
name: 'b',
created_at: new Date(),
};
expectType<{id: string; name: string; created_at: Date}>(fixture);
}

{
type Single = {value: number};
expectTypeOf<RenameKey<Single, 'value', 'amount'>>().toEqualTypeOf<{amount: number}>();
}

// Preserves the optional modifier.
{
type Source = {
id: string;
label?: string;
};

expectTypeOf<RenameKey<Source, 'label', 'name'>>().toEqualTypeOf<{id: string; name?: string}>();
}

// Preserves the readonly modifier.
{
type Source = {
readonly id: string;
name: string;
};

expectTypeOf<RenameKey<Source, 'id', 'identifier'>>().toEqualTypeOf<{readonly identifier: string; name: string}>();
}

// `readonly` and optional combine and travel together to the new name.
{
type Source = {readonly a?: number; x: string};
expectTypeOf<RenameKey<Source, 'a', 'b'>>().toEqualTypeOf<{readonly b?: number; x: string}>();
}

// An explicit `| undefined` value stays required; it does not become optional.
{
type Source = {a: string | undefined; x: number};
expectTypeOf<RenameKey<Source, 'a', 'b'>>().toEqualTypeOf<{b: string | undefined; x: number}>();
}

// Renaming is shallow: a nested object value is preserved verbatim.
{
type Source = {a: {nested: number; deep: {x: string}}; c: boolean};
expectTypeOf<RenameKey<Source, 'a', 'b'>>().toEqualTypeOf<{b: {nested: number; deep: {x: string}}; c: boolean}>();
}

// Symbol target key.
{
const symbolKey: unique symbol = Symbol('symbolKey');
type SymbolKey = typeof symbolKey;
type Source = {tag: string};
expectTypeOf<RenameKey<Source, 'tag', SymbolKey>>().toEqualTypeOf<{[symbolKey]: string}>();
}

// Number target key.
{
type Source = {first: number};
expectTypeOf<RenameKey<Source, 'first', 0>>().toEqualTypeOf<{0: number}>();
}

// Symbol source key.
{
const tag: unique symbol = Symbol('tag');
type Tag = typeof tag;
type Source = {[tag]: 'x'; name: string};
expectTypeOf<RenameKey<Source, Tag, 'label'>>().toEqualTypeOf<{label: 'x'; name: string}>();
}

// Distributes over union members.
{
type Event =
| {kind: 'click'; target: string}
| {kind: 'submit'; target: HTMLFormElement};

type Renamed = RenameKey<Event, 'target', 'element'>;
expectTypeOf<Renamed>().toEqualTypeOf<
| {kind: 'click'; element: string}
| {kind: 'submit'; element: HTMLFormElement}
>();
}

// The source key need not exist in every union member.
{
type Variants =
| {kind: 'a'; payload: string}
| {kind: 'b'};

type Renamed = RenameKey<Variants, 'payload', 'value'>;
expectTypeOf<Renamed>().toEqualTypeOf<
| {kind: 'a'; value: string}
| {kind: 'b'}
>();
}

// Renaming a key to itself is a no-op.
{
type Source = {a: number; b: string};
expectTypeOf<RenameKey<Source, 'a', 'a'>>().toEqualTypeOf<{a: number; b: string}>();
}

{
type Source = {a: number};
// @ts-expect-error 'missing' is not a key of the source.
type Bad = RenameKey<Source, 'missing', 'renamed'>;
}

{
type Source = {a: number} | {a: string};
// @ts-expect-error 'b' is not a key of any union member.
type Bad = RenameKey<Source, 'b', 'c'>;
}

// Renaming onto an existing key is rejected: collapsing two keys would also
// collapse their modifiers, silently turning a required key optional.
{
type Source = {a: number; b: string};
// @ts-expect-error 'b' already exists on the source.
type Bad = RenameKey<Source, 'a', 'b'>;
}

{
type Source = {a: number} | {b: string};
// @ts-expect-error 'b' exists in a sibling union member, so the rename is rejected union-wide.
type Bad = RenameKey<Source, 'a', 'b'>;
}

{
type Source = any;
// @ts-expect-error `any` could hold the target key, so the rename can't be proven safe.
type Bad = RenameKey<Source, 'a', 'b'>;
}

{
type Source = never;
// @ts-expect-error `never` has no keys to rename.
type Bad = RenameKey<Source, 'a', 'b'>;
}

{
type Source = unknown;
// @ts-expect-error `unknown` has no keys to rename.
type Bad = RenameKey<Source, 'a', 'b'>;
}

// A string index signature means every string key already exists, so any rename is a collision.
{
type Source = {[key: string]: number; a: number};
// @ts-expect-error target collides with the index signature.
type Bad = RenameKey<Source, 'a', 'b'>;
}

// Renaming is shallow; nested keys are not addressable.
{
type Source = {a: {nested: number}};
// @ts-expect-error 'nested' is not a top-level key.
type Bad = RenameKey<Source, 'nested', 'x'>;
}

/* eslint-enable no-lone-blocks */
Loading