Description
Search Terms
Distributive keyof, ValueOf, PickWhen, OmitWhen
Partial results...
- [Feature] Utility type for T[keyof T] or new keyword valueof #37642
- Add
KeyOf
andValueOf
definitions to built-in lib #31438
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 extendsC
and which keys intersects withK
.
C
defaults toany
K
defaults to the combined keys of each element ofU
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 constraintC
and eventually restrict the keys that intersect withK
.
C
defaults toany
K
defaults to the combined keys of each element ofU
- 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 withPickWhen
behaviour:Currently: Constructs a type by extracting from
T
all properties that are assignable toU
.Proposed Change: Constructs a type by extracting from
T
all types that are assignable toU
.
OmitWhen<U, C?, K?>
For each member of the union
U
,Omit
the key-values when the value matches the constraintC
and eventually restrict the keys that intersect withK
.
C
defaults toany
K
defaults to the combined keys of each element ofU
- 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.