ts-essentials / ts-essentials

All essential TypeScript types in one place 🤙

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Replace<Type, Keys, Replacement> that will replace all keys "Keys" of type "Type" with "Replacement"

xenoterracide opened this issue · comments

I find myself wanting to do things like StrictReplaceType<Foo,BarConstructorOpts,'bar'>

where Foo looks something like

class Foo {
   bar: Bar
}

would be good to have a deep variant too, and I'm trying to create a deep constructor factory for both.

Found this on SO, and generall seems to do what I want (replaced Exclude with StrictExclude though I'm not certain it's really valuable here)

type Identity<T> = { [P in keyof T]: T[P] };
type Replace<T, K extends keyof T, TReplace> = Identity<
  Pick<T, StrictExclude<keyof T, K>> & {
    [P in K]: TReplace;
  }
>;

@Beraliv I've got an implementation of this and a deep version. Not sure if I should work on a patch

import { Builtin, StrictExclude } from 'ts-essentials';

export type Identity<T> = { [P in keyof T]: T[P] };
export type Replace<T, K extends keyof T, TReplace> = Identity<
  Pick<T, StrictExclude<keyof T, K>> & {
    [P in K]: TReplace;
  }
>;

export type DeepReplace<T, TReplace> = T extends Builtin
  ? TReplace
  : T extends Map<infer K, infer V>
  ? Map<K, DeepReplace<V, TReplace>>
  : T extends ReadonlyMap<infer K, infer V>
  ? ReadonlyMap<K, DeepReplace<V, TReplace>>
  : T extends WeakMap<infer K, infer V>
  ? WeakMap<K, DeepReplace<V, TReplace>>
  : T extends Set<infer U>
  ? Set<DeepReplace<U, TReplace>>
  : T extends ReadonlySet<infer U>
  ? ReadonlySet<DeepReplace<U, TReplace>>
  : T extends WeakSet<infer U>
  ? WeakSet<DeepReplace<U, TReplace>>
  : T extends Promise<infer U>
  ? Promise<DeepReplace<U, TReplace>>
  : T extends object
  ? { [K in keyof T]-?: DeepReplace<T[K], TReplace> }
  : TReplace;

Hey @xenoterracide!

Apologies for the late response, let me quickly come back to you later today!

I will be able to properly test it and come back to you with the feedback!

no worries. I've realized I might have another use case this doesn't cover though, and I'm not sure you've got prior art for that

type Foo = DeepReplace<Bar, {
   baz: Array<Bar>
}>

might be useful, but only with static keyname checking... otherwise you'd have to replace them one at a time

however, then that kind of conflicts with this

export interface Entity<Id> {
  id: Id;
  createdAt: Date;
  modifiedAt: Date;
}

export type EntityOmit = DeepReplace<Entity<never>, never>;

should be possible to have an API something like

DeepReplace<Entity<never>, never, 'all'>
DeepReplace<Entity<never>, { createdAt: string }, 'filter'>

P.S. Happy to try creating a PR, I've just come to hate doing those until someone tells me they're interested due to the amount of work required. Current employer is letting me.

hmm... I think that implementation of Replace might be replacing a readonly foo: readonly[] to foo: [] when trying Replace<'foo', []> which should result in readonly foo: [] (imho)

Hey again!

Thank you for the suggestions!

I'm currently looking at how it can fit in what we already have in ts-essentials.

So now we have DeepOmit, DeepPick which use DeepModify and I would want to stick to this approach in case we want to introduce another DeepCandidate.

In this case the structure will be slightly different and there will be some limitations to the solution:

  1. You need to keep the structure of the type you want to replace, e.g. DeepReplace<ComplexObject, { simple: string; nested: { func: () => number }>

  2. You won't be able to use self referencing types, e.g. recursive type JSONValue – https://tsplay.dev/wRze7w

hmm... I think that implementation of Replace might be replacing a readonly foo: readonly[] to foo: [] when trying Replace<'foo', []> which should result in readonly foo: [] (imho)

The problem that you've mentioned about readonly above is connected to the implementation as { [K in keyof T]-?: DeepReplace<T[K], TReplace> } isn't homomorphic, you can read more about it here – https://stackoverflow.com/questions/59790508/what-does-homomorphic-mapped-type-mean/59791889#59791889

The implementation of Replace itself can look similar, but you will need to explicitly say which fields you want assign to what – https://tsplay.dev/NV75lm. The reason I want to implement it this way as it allows you to replace fields with different values.

Do you think it will be useful in you case? Can you please add more examples where you find it useful?

I think it would be useful at least in some of the use cases.

However one of my use cases is to take a regular type or interface and turn it into the input for a DeepOmit, and providing never for every single thing doesn't feel very efficient. Which is why I was considering a more expansive API, if a full level of filter was really needed.

yeah, I was going to try DeepModify then I realized it wasn't exported, so I couldn't play with it locally, and I didn't come into that immediately ;).

Maybe the never case would be better resolved by a different type, like a Never<T> and a DeepNever<T> (not sure I have a case for a never that isn't all, I just haven't had a deep structure yet). I added the never case, but hopefully you can see why I don't think it's code-ly efficient.

I added one other example, as I spend most of my time replacing array types with NonEmptyArray, or never-ing.

should I patch this? should DeepNever be a different type?

Apologies for the late response, let me come back to you later today

yeah, I was going to try DeepModify then I realized it wasn't exported, so I couldn't play with it locally, and I didn't come into that immediately

DeepModify is not exported on purpose but it's a way to work with types like DeepOmit and DeepPick.

I added the never case, but hopefully you can see why I don't think it's code-ly efficient.

I understand your reasons, but it's not too scalable once you have edge cases, that's why I'm worried to use such an approach.

E.g. you have one never that you want to replace and another one isn't really working for you. In types we don't have high order types (we kind of have HKTs but they're usually predefined so it's almost impossible to do something when you don't know about the type in advance) so we cannot have callbacks alternatives as in JavaScript.

That's why I suggest you to do what's already used in DeepOmit and DeepPick – you will need to repeat the structure, it will restrict and limit the usage but will increase scalability when you have more adjustments.

If you have simpler cases, e.g. export type EntityOmit = DeepReplace<Entity<never>, TSomethingElse>;, you can simply use generic types instead, e.g. Entity<TSomethingElse>

export interface Entity<Id> {
  id: Id;
  createdAt: Date;
  modifiedAt: Date;
}

export type EntityOmit = DeepReplace<Entity<never>, never>;
DeepReplace<Entity<never>, never, 'all'>
DeepReplace<Entity<never>, { createdAt: string }, 'filter'>

Given these examples, could you please let me know what you expect from these examples? You've mentioned all and filter here, but it's not obvious what you want to get as an output?

P.S. Happy to try creating a PR, I've just come to hate doing those until someone tells me they're interested due to the amount of work required. Current employer is letting me.

Before jumping on the code I want to understand all the cases, it's still unclear for me. Can I ask you to write down the following please?

  • Input, or what you try to change
  • Manipulation, or how you want to change the input
  • Expected result, or what you want to get at the end

Once we have it as a short list of examples, let's think of API and once it's agreed, I'm happy to start working on it

@xenoterracide Hey!

I think we got a second discussion and totally avoided the original suggestion about StrictReplaceType

Will it still be valuable for you?

If you want to focus on DeepReplace instead, would it be okay to close this one as "won't do", could you please create a separate issue with all requirements that you need for that?

Thank you in advance!

@xenoterracide hey!

Can you please highlight the situations where Replace would be helpful for you?

Because I think StrictOmit (analogue of Omit within ts-essentials) and Record together will be a good implementation for your Replace:

type Replace<Type, Keys extends keyof Type, TReplace> = StrictOmit<Type, Keys> & Record<Keys, TReplace>

So I think unless you disagree, I will close this issue