Skip to content

Enrich TypeScript Utility Types #39305

Closed
Closed
@SalathielGenese

Description

@SalathielGenese

Search Terms

Distributive keyof, ValueOf, PickWhen, OmitWhen

Partial results...

Suggestion

I'd like to see TypeScript default typeset richer so that some common takes comes handy and universal.

KeyOf<U, C?, K?>

Get the keys of each member of the union U, mapped to a value whose type extends C and which keys intersects with K.

  • C defaults to any
  • K defaults to the combined keys of each element of U

In #31438, I stressed the need to collect keys or union type. Today, I have it evolve by supporting conditions on the value they map to. I can collect all the keys of an entity that maps to another entity and array of entities.

type KeyOf<U, C = any, K extends (U extends any ? keyof U : never) = (U extends any ? keyof U : never)> =
  C extends any ? U extends any ? K extends any ? U[K] extends C ? K : never : never : never : never;

e.g:

type testKeyOf = KeyOf<{ a: 'Aa' } | { b: 'Bb' } | { ó: 'Aa' }, 'Aa'>; //  "a" | "ó"

ValueOf<U, K?>

A distributive version of Pick.

Also in #31438 also, I suggested this type along the KeyOf. Again, I augmented it today with the ability to narrow down which keys to pick, if applicable to the union member.

type ValueOf<U, K extends KeyOf<U> = KeyOf<U>> = U extends any ? K extends keyof U ? U[K] : never : never;

e.g:

type testValueOf = ValueOf<{ a: 'Aa' } | { a: 0, b: 'Bb' }, 'a'>; // "Aa" | 0

PickWhen<U, C?, K?>

For each member of the union U, Pick the key-values when the value matches the constraint C and eventually restrict the keys that intersect with K.

  • C defaults to any
  • K defaults to the combined keys of each element of U
  • Union member that would transform into empty objects are commuted into never

Pick does quite a great job by containing on the keys, PickWhen proposes to constrain on the value type, while still allowing to narrow down the keys to picking from, and keeping the union structure.

type PickWhen<U, C = any, K extends KeyOf<U> = KeyOf<U>> =
  U extends any ? K & KeyOf<U, C> extends never ? never : { [k in K & KeyOf<U, C>]: U[k] } : never;

e.g:

type testPickWhen = PickWhen<{ a: 'Aa' } | { a: 0, b: 'Bb' }, 'Aa' | 'Bb'>; // { a: 'Aa' } | { b: 'Bb' }

NOTES: Rephrase the description of Extract which would be confusing with PickWhen behaviour:

Currently: Constructs a type by extracting from T all properties that are assignable to U.

Proposed Change: Constructs a type by extracting from T all types that are assignable to U.

OmitWhen<U, C?, K?>

For each member of the union U, Omit the key-values when the value matches the constraint C and eventually restrict the keys that intersect with K.

  • C defaults to any
  • K defaults to the combined keys of each element of U
  • Union member that would transform into empty objects are commuted into never

OmitWhen it to Omit what PickWhen is to Pick

type OmitWhen<U, C = any, K extends KeyOf<U> = KeyOf<U>> =
  U extends any ? Exclude<K & keyof U, KeyOf<U, C>> extends never ? never : { [k in Exclude<K & keyof U, KeyOf<U, C>>]: U[k] } : never;

e.g:

type testOmitWhen = OmitWhen<{ a: 'Aa' } | { a: 0, b: 'Bb' }, 0 | 'Bb'>; // => { a: 'Aa' } | { b: 'Bb' }

Use Cases

Provided along with each proposed type.

Examples

I have put it all together here, on TypeScript Playground.

export interface Model<ID = number | string>
{
  id: ID;

  createdAt?: Date;

  updatedAt?: Date;
}

export type ModelFind<M extends Model> = Partial<OmitWhen<M, Model | Model[]>>;

export type ModelCreate<M extends Model> = OmitWhen<M, Model | Model[], Exclude<KeyOf<M>, 'id' | 'createdAt' | 'updatedAt'>>;

export type ModelUpdate<M extends Model> = {
  [k in Exclude<keyof M, 'createdAt' | 'updatedAt'>]?: M[k] extends Model ? ModelUpdate<M[k]>
  : M[k] extends Model[] ? ModelUpdate<M[k][number]>[] : M[k];
};
interface Class extends Model<number>
{
  level: string;

  school: School;

  students: Student[];

  schoolId: School['id'];
}

interface School extends Model<number>
{
  isPublic: boolean;

  classes: Class[];

  name: string;
}

interface Student extends Model<number>
{
  firstName: string;

  lastName: string;

  classId: Class['id'];

  class: Class;
}

// Check out the types below

type ClassFind = ModelFind<Class>;
type SchoolFind = ModelFind<School>;
type StudentFind = ModelFind<Student>;

type ClassCreate = ModelCreate<Class>;
type SchoolCreate = ModelCreate<School>;
type StudentCreate = ModelCreate<Student>;

type ClassUpdate = ModelUpdate<Class>;
type SchoolUpdate = ModelUpdate<School>;
type StudentUpdate = ModelUpdate<Student>;

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behaviour of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions