Skip to content

Commit

Permalink
feat(types): infer nullable creation attributes as optional (#14147)
Browse files Browse the repository at this point in the history
  • Loading branch information
ephys committed Feb 24, 2022
1 parent a65339d commit b8bce31
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 15 deletions.
20 changes: 19 additions & 1 deletion docs/manual/other-topics/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ Important things to know about `InferAttributes` & `InferCreationAttributes` wor

`InferCreationAttributes` works the same way as `AttributesOf` with one exception: Properties typed using the `CreationOptional` type
will be marked as optional.
Note that attributes that accept `null`, or `undefined` do not need to use `CreationOptional`:

```typescript
class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
declare firstName: string;

// there is no need to use CreationOptional on firstName because nullable attributes
// are always optional in User.create()
declare lastName: string | null;
}

// ...

await User.create({
firstName: 'Zoé',
// last name omitted, but this is still valid!
});
```

You only need to use `CreationOptional` & `NonAttribute` on class instance fields or getters.

Expand Down Expand Up @@ -106,7 +124,7 @@ class User extends Model<InferAttributes<User, { omit: 'projects' }>, InferCreat
// Since TS cannot determine model association at compile time
// we have to declare them here purely virtually
// these will not exist until `Model.init` was called.
declare getProjects: HasManyGetAssociationsMixin<Project>; // Note the null assertions!
declare getProjects: HasManyGetAssociationsMixin<Project>;
declare addProject: HasManyAddAssociationMixin<Project, number>;
declare addProjects: HasManyAddAssociationsMixin<Project, number>;
declare setProjects: HasManySetAssociationsMixin<Project, number>;
Expand Down
6 changes: 3 additions & 3 deletions src/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { HookReturn, Hooks, ModelHooks } from './hooks';
import { ValidationOptions } from './instance-validator';
import { IndexesOptions, QueryOptions, TableName } from './dialects/abstract/query-interface';
import { Sequelize, SyncOptions } from './sequelize';
import { Col, Fn, Literal, Where, MakeUndefinedOptional, AnyFunction } from './utils';
import { Col, Fn, Literal, Where, MakeNullishOptional, AnyFunction } from './utils';
import { LOCK, Transaction, Op } from './index';
import { SetRequired } from './utils/set-required';

Expand Down Expand Up @@ -2784,7 +2784,7 @@ export abstract class Model<TModelAttributes extends {} = any, TCreationAttribut
*
* @param values an object of key value pairs
*/
constructor(values?: MakeUndefinedOptional<TCreationAttributes>, options?: BuildOptions);
constructor(values?: MakeNullishOptional<TCreationAttributes>, options?: BuildOptions);

/**
* Get an object representing the query for this instance, use with `options.where`
Expand Down Expand Up @@ -3174,7 +3174,7 @@ type InternalInferAttributeKeysFromFields<M extends Model, Key extends keyof M,
* @example
* function buildModel<M extends Model>(modelClass: ModelStatic<M>, attributes: CreationAttributes<M>) {}
*/
export type CreationAttributes<M extends Model | Hooks> = MakeUndefinedOptional<M['_creationAttributes']>;
export type CreationAttributes<M extends Model | Hooks> = MakeNullishOptional<M['_creationAttributes']>;

/**
* Returns the creation attributes of a given Model.
Expand Down
27 changes: 16 additions & 11 deletions src/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,40 +127,45 @@ export class Where extends SequelizeMethod {
export type AnyFunction = (...args: any[]) => any;

/**
* Returns all shallow properties that accept `undefined`.
* Does not include Optional properties, only `undefined`.
* Returns all shallow properties that accept `undefined` or `null`.
* Does not include Optional properties, only `undefined` or `null`.
*
* @example
* type UndefinedProps = UndefinedPropertiesOf<{
* type UndefinedProps = NullishPropertiesOf<{
* id: number | undefined,
* createdAt: string | undefined,
* firstName: string,
* firstName: string | null, // nullable properties are included
* lastName?: string, // optional properties are not included.
* }>;
*
* // is equal to
*
* type UndefinedProps = 'id' | 'createdAt';
* type UndefinedProps = 'id' | 'createdAt' | 'firstName';
*/
export type UndefinedPropertiesOf<T> = {
[P in keyof T]-?: undefined extends T[P] ? P : never
export type NullishPropertiesOf<T> = {
[P in keyof T]-?: undefined extends T[P] ? P
: null extends T[P] ? P
: never
}[keyof T];

/**
* Makes all shallow properties of an object `optional` if they accept `undefined` as a value.
* Makes all shallow properties of an object `optional` if they accept `undefined` or `null` as a value.
*
* @example
* type MyOptionalType = MakeUndefinedOptional<{
* id: number | undefined,
* name: string,
* firstName: string,
* lastName: string | null,
* }>;
*
* // is equal to
*
* type MyOptionalType = {
* // this property is optional.
* id?: number | undefined,
* name: string,
* firstName: string,
* // this property is optional.
* lastName?: string | null,
* };
*/
export type MakeUndefinedOptional<T extends object> = Optional<T, UndefinedPropertiesOf<T>>;
export type MakeNullishOptional<T extends object> = Optional<T, NullishPropertiesOf<T>>;
10 changes: 10 additions & 0 deletions test/types/infer-attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class User extends Model<InferAttributes<User, { omit: 'omittedAttribute' | 'omi
declare optionalArrayAttribute: CreationOptional<string[]>;
declare mandatoryArrayAttribute: string[];

// note: using CreationOptional here is unnecessary, but we still ensure that it works.
declare nullableOptionalAttribute: CreationOptional<string | null>;

declare nonAttribute: NonAttribute<string>;
Expand Down Expand Up @@ -118,3 +119,12 @@ expectTypeOf<UserCreationAttributes>().not.toHaveProperty('staticMethod');
// ensure branding does not break null
const brandedString: NonAttribute<string | null> = null;
}

{
class User2 extends Model<InferAttributes<User2>, InferCreationAttributes<User2>> {
declare nullableAttribute: string | null;
}

// this should work, all null attributes are optional in Model.create
User2.create({});
}

0 comments on commit b8bce31

Please sign in to comment.