Skip to content

Enrich TypeScript Utility Types #39305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
SalathielGenese opened this issue Jun 28, 2020 · 4 comments
Closed
5 tasks done

Enrich TypeScript Utility Types #39305

SalathielGenese opened this issue Jun 28, 2020 · 4 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@SalathielGenese
Copy link

SalathielGenese commented Jun 28, 2020

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.
@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript labels Jun 29, 2020
@RyanCavanaugh
Copy link
Member

We have decided to not add any additional utility types unless they are needed for declaration emit. Every time we add a new utility type, there are ~four different forms to choose from, people split roughly equally on which is "best", and then 75% of people get mad that we added the "wrong" one and blocked them from adding the "right" one to their project's global scope - given that situation, we think it's better for people to just add whichever utility types they need to their own project.

@SalathielGenese
Copy link
Author

Thank you @RyanCavanaugh . It is legit.

@SalathielGenese
Copy link
Author

Please, any TS utility types out there to recommend suggesting this at ?

@KilianKilmister
Copy link

@SalathielGenese type-fest has implementations of these

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants