Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@
},
"rules": {
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"import/first": "error",
"import/no-absolute-path": "error",
"import/no-cycle": "error",
Expand Down
1 change: 0 additions & 1 deletion bench/isPromiseLike.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ function isPromiseLike2<T>(input: unknown): input is PromiseLike<T> {
const resolvedPromise = Promise.resolve();

const promiseLikeObject = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
then(_onfulfilled: unknown, _onrejected: unknown): void {
// noop
},
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"url": "https://github.com/webdeveric/utils/issues"
},
"homepage": "https://github.com/webdeveric/utils/#readme",
"packageManager": "pnpm@11.16.0+sha512.b767e9a98fc87aec0f42daefcd0e84941c2cdb45d73c4e1e68860fb2bdfdbe032ab592105479e5e05c237f740c8a5ffdbbb52385797ddc2296745a1bbb883e8d",
"packageManager": "pnpm@11.19.0+sha512.7881f3ed590d472c4a955e2b88b2121791116066dcc88cbca3849ec9b60f1bbaa6d2ccb221fa91da4e1c65bef2bcbe379365aea7ac539c7bf86dedc3a1b22dce",
"scripts": {
"clean": "rimraf ./dist/ ./cache/ ./coverage/",
"prebuild": "pnpm clean",
Expand Down Expand Up @@ -95,22 +95,22 @@
"@webdeveric/prettier-config": "^0.4.0",
"@webdeveric/ts-data-structures": "^1.0.1",
"commitlint": "^21.2.1",
"commitlint-plugin-cspell": "^0.9.3",
"commitlint-plugin-cspell": "^0.9.4",
"conventional-changelog-conventionalcommits": "^9.3.1",
"cspell": "^10.0.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-import": "^2.32.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.1.0",
"jsdom": "^30.0.1",
"lint-staged": "^17.3.0",
"prettier": "^3.9.6",
"rimraf": "^6.1.3",
"semantic-release": "^25.0.8",
"typedoc": "^0.28.20",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"validate-package-exports": "^1.3.2",
"validate-package-exports": "^1.3.3",
"vitest": "^4.1.10"
}
}
764 changes: 395 additions & 369 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions src/defineProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {
ApplyPropertyAttribute,
GetPropertyRecordFromAttribute,
PropertyAttributeMap,
RetainMutability,
} from './types/objects.js';
import type { Pretty, UnionToIntersection } from './types/utils.js';

export type WithPropertyMap<Type, AttributeMap extends PropertyAttributeMap> = Omit<Type, keyof AttributeMap> &
UnionToIntersection<
{
[Property in keyof AttributeMap]: ApplyPropertyAttribute<
GetPropertyRecordFromAttribute<Type, Property, AttributeMap[Property]>,
AttributeMap[Property],
RetainMutability<Type, Property>
>;
}[keyof AttributeMap]
>;

/**
* Type-safe wrapper around `Object.defineProperties()` that reflects the new properties in the returned type.
*
* @example
* ```ts
* const user = defineProperties({ name: 'Test' }, {
* age: { value: 42 },
* isAdmin: { get: () => false },
* title: {
* writable: true,
* },
* });
* // user: { name: string, readonly age: number, readonly isAdmin: boolean, title: unknown }
* ```
*/
export function defineProperties<Type, AttributeMap extends PropertyAttributeMap>(
input: Type,
attributeMap: AttributeMap,
): Pretty<WithPropertyMap<Type, AttributeMap>> {
return Object.defineProperties(input, attributeMap) as WithPropertyMap<Type, AttributeMap>;
}
39 changes: 39 additions & 0 deletions src/defineProperty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
ApplyPropertyAttribute,
GetPropertyRecordFromAttribute,
PropertyAttribute,
RetainMutability,
} from './types/objects.js';
import type { Pretty } from './types/utils.js';

export type WithProperty<Type, Property extends PropertyKey, Attribute extends PropertyAttribute> = Omit<
Type,
Property
> &
ApplyPropertyAttribute<
GetPropertyRecordFromAttribute<Type, Property, Attribute>,
Attribute,
RetainMutability<Type, Property>
>;

/**
* Type-safe wrapper around `Object.defineProperty` that reflects the new property in the returned type.
*
* @example
* ```ts
* const user = defineProperty({ name: 'Test' }, 'age', { value: 42 });
* // user: { name: string, readonly age: number }
* ```
*
* ```ts
* const user = defineProperty({ name: 'Test' }, 'title', { writable: true });
* // user: { name: string, title: unknown }
* ```
*/
export function defineProperty<Type, Property extends PropertyKey, Attributes extends PropertyAttribute>(
input: Type,
property: Property,
attributes: Attributes,
): Pretty<WithProperty<Type, Property, Attributes>> {
return Object.defineProperty(input, property, attributes) as WithProperty<Type, Property, Attributes>;
}
2 changes: 1 addition & 1 deletion src/graphemeLength.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
export function graphemeLength(input: string): number {
let count = 0;

// eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/naming-convention
for (const _ of segmenter.segment(input)) {
count++;
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export * from './counter.js';
export * from './dedent.js';
export * from './deepDecodeURI.js';
export * from './deepFreeze.js';
export * from './defineProperties.js';
export * from './defineProperty.js';
export * from './delay.js';
export * from './delayAnimationFrame.js';
export * from './delayAnimationFrames.js';
Expand Down
2 changes: 1 addition & 1 deletion src/predicate/assume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
* assume<string>(42); // true (no runtime check is performed)
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const assume = <Type>(_input: unknown, ..._args: any[]): _input is Type => true;
68 changes: 67 additions & 1 deletion src/types/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Primitive } from './common.js';
import type { UnknownRecord } from './records.js';
import type { AutoCompletableString, CamelCase } from './strings.js';
import type { KeyValueTuple } from './tuples.js';
import type { CanBeUndefined, IfNever, Pretty } from './utils.js';
import type { CanBeUndefined, IfNever, IsReadonlyKey, Pretty } from './utils.js';

export type Assign<Target, Source> = IfNever<Target, Source, Omit<Target, keyof (Target | Source)> & Source>;

Expand Down Expand Up @@ -183,3 +183,69 @@ export type MergeArrayWithObject<Left extends unknown[], Right extends UnknownRe
: Right[Key]
: Right[Key];
};

export type GetPropertyType<Type, Property extends PropertyKey, Default = unknown> = Property extends keyof Type
? Type[Property]
: Default;

/**
* Whether redefining `Property` on `Type` should keep the property mutable when
* the new attribute doesn't say either way. This mirrors `Object.defineProperty()`,
* which keeps a property's existing `writable` flag when it isn't part of the new
* descriptor and the property already exists.
*/
export type RetainMutability<Type, Property extends PropertyKey> = Property extends keyof Type
? IsReadonlyKey<Type, Property> extends true
? false
: true
: false;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyThisType = ThisType<any>;

export type PropertyAttribute = PropertyDescriptor & AnyThisType;

export type PropertyAttributeMap = PropertyDescriptorMap & AnyThisType;

export type ApplyPropertyAttribute<
Type,
Attribute extends PropertyAttribute,
Retain extends boolean = false,
> = Attribute extends
| {
writable: true;
}
| {
set(value: never): void;
}
? Type
: Attribute extends {
writable: false;
}
? Readonly<Type>
: Retain extends true
? Type
: Readonly<Type>;

export type GetTypeFromAttributes<Attribute extends PropertyAttribute, DefaultType = unknown> = Attribute extends
| {
value: infer Type;
}
| { get(): infer Type }
| { set(value: infer Type): void }
? Type
: DefaultType;

export type GetPropertyRecordFromAttribute<
Type,
Property extends PropertyKey,
Attribute extends PropertyAttribute,
DefaultType = GetPropertyType<Type, Property>,
> = Attribute extends
| {
value: infer PropertyType;
}
| { get(): infer PropertyType }
| { set(value: infer PropertyType): void }
? Record<Property, PropertyType>
: Record<Property, DefaultType>;
4 changes: 1 addition & 3 deletions src/types/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export type Replace<

export type Strip<Type extends string, Remove extends string> = Replace<Type, Remove, EmptyString>;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type FirstChar<Type extends string> = Type extends `${infer First}${infer _Rest}` ? First : never;

export type AddSpaceAroundNumbers<Type extends string> = Type extends `${infer First}${infer Rest}`
Expand Down Expand Up @@ -98,7 +97,6 @@ export type PascalCase<Type extends string, Delimiter extends string = Space> =

export type StringLength<Type extends string, Accumulator extends never[] = []> = string extends Type
? never
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
Type extends `${infer _}${infer Rest}`
: Type extends `${infer _}${infer Rest}`
? StringLength<Rest, [never, ...Accumulator]>
: Accumulator['length'];
1 change: 0 additions & 1 deletion src/types/tuples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type KeyValueTuple<K extends PropertyKey = PropertyKey, V = unknown> = [k

export type TupleToArray<T extends any[]> = T[number][];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type IfTuple<Type, T, F> = Type extends readonly [...infer _] ? (number extends Type['length'] ? F : T) : F;

export type IsTuple<Type> = IfTuple<Type, true, false>;
10 changes: 10 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export type IfPromise<Type, T, F> = Type extends Promise<any> ? T : F;

export type IfSame<Left, Right, T, F> = [Left] extends [Right] ? ([Right] extends [Left] ? T : F) : F;

/**
* Strict type equality check. Unlike `IfSame`, this distinguishes `readonly` modifiers,
* since ordinary mutual `extends` checks ignore them.
*/
export type Equals<Left, Right> =
(<Type>() => Type extends Left ? 1 : 2) extends <Type>() => Type extends Right ? 1 : 2 ? true : false;

export type NotPromise<Type> = Type extends Promise<any> ? never : Type;

export type CanBeUndefined<Type, T, F> = Type | undefined extends Type ? T : F;
Expand All @@ -31,6 +38,9 @@ export type Writable<Type> = {

export type Unwritable<Type> = Type extends Writable<infer Inner> ? Readonly<Inner> : Readonly<Type>;

export type IsReadonlyKey<Type, Keys extends keyof Type> =
Equals<Pick<Type, Keys>, { -readonly [Property in Keys]: Type[Property] }> extends true ? false : true;

export type GetIndex<Type> = {
[
Key in keyof Type as symbol extends Key ? Key : string extends Key ? Key : number extends Key ? Key : never
Expand Down
Loading