8

I have a type:

type first = {
    one: number;
    two: string;
    three: {
        four: string,
        five: number,
    }
}

It is applicable to one instance of a variable that I declare in one part of my application, but not exactly applicable to another (second) instance of a variable.

The type that would be suitable for second instance of a variable would look like this:

type second = {
    one: number;
    two: string;
    three: {
        four: string,
        five: number[], //difference
    }
}

I don't want to declare a new type from scratch for a small difference and would like to assimilate the existing type first by replacing the type of property three.

I tried to do it this way:

type second = Pick<first, Exclude<keyof first, 'three'>> & { 
    three: {
        four: string,
        five: number[], //difference
    } 
}

But it gives me an error and I get this type definition on hover:

type second = {
    one: number;
    two: string;
    three: {
        four: string,
        five: number,
    };
    three: {
        four: string,
        five: number,
    };
}

Notice 2 properties three.

What am I doing wrong?

Eduard
  • 8,437
  • 10
  • 42
  • 64
  • 1
    Can't really reproduce the issue : http://www.typescriptlang.org/play/#src=type%20first%20%3D%20%7B%0D%0A%09one%3A%20number%3B%0D%0A%09two%3A%20string%3B%0D%0A%09three%3A%20%7B%0D%0A%09%09four%3A%20string%2C%0D%0A%09%09five%3A%20number%2C%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Atype%20second%20%3D%20Pick%3Cfirst%2C%20Exclude%3Ckeyof%20first%2C%20'three'%3E%3E%20%26%20%7B%20%0D%0A%09three%3A%20%7B%20four%3A%20string%2C%20five%3A%20number%5B%5D%2C%20%20%7D%0D%0A%7D%0D%0Alet%20d%3A%20second%20%3D%20%7Bone%3A%201%2Ctwo%3A%20%22%22%2Cthree%20%3A%20%7Bfour%3A%20%22%22%2Cfive%3A%20%5B0%5D%7D%7D – Titian Cernicova-Dragomir Jul 30 '18 at 17:44
  • @TitianCernicova-Dragomir I think I got confused by the schema of the type "second" displayed by Typescript, which did not resolve the "Pick" part leaving its definition instead of the result of its application (I had the text "Pick<>" in the schema). – Eduard Jul 31 '18 at 10:39
  • Yeah ... exactly what is expanded in the types is still a mystery for me personally... – Titian Cernicova-Dragomir Jul 31 '18 at 10:42

5 Answers5

10

You solution should work, I just have two observations:

  1. You can use the same approach one level down using type queries, no need to rewrite the inner object

  2. Careful with intersection types, the assumption that they behave exactly the same as hand crafted type is not always true. See this question. We can get around such issues flattening the type using an additional mapped type.

With these in mind this is what I would do:

type first = {
    one: number;
    two: string;
    three: {
        four: string,
        five: number,
    }
}

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

type second = Replace<first, 'three', Replace<first['three'], 'five', number[]>>
let d: second = {
    one: 1,
    two: "",
    three : {
        four: "",
        five: [0]
    }
}

Playground link

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Why did you call "Flatten" type this way? We do not flatten the object here, do we? Flattening is making an object one level shallower as far as I understand. But it seems that in your example we kind of duplicate the object by remapping? – Eduard Jul 31 '18 at 10:01
  • 2
    @Eduard flatten was the wrong name perhaps .. it's flattening out the intersection types ...I changed the name to `Identity` – Titian Cernicova-Dragomir Jul 31 '18 at 10:26
  • Thank you for the clarification! Now I understand. Really appreciate your answers! – Eduard Jul 31 '18 at 10:34
  • 1
    It was a real pain to read mapped types before. Thanks for this "Identity" trick! I am going to use it everywhere now – Romain Vincent Jul 14 '22 at 15:31
8

You could just try to replace a part of the type definition, by using a union. The following is an oversimplification:

type SecondType = FirstType & {
   three: {
        four: string,
        five: number[],
    }
  };

The solution mentioned above will get you in trouble. A simple union doesn't really replace property "three", it actually creates 2 conflicting definitions for properties "three" and "five". Your IDE will give you warnings whenever you try to access them.

A solution, is to remove the property and then merge it back in:

type SecondType = 
   Omit<FirstType, 'three'> & 
   { three: {
        four: string,
        five: number[]
   }};
bvdb
  • 22,839
  • 10
  • 110
  • 123
4

Building on Titian's answer.

If stacking multiple Overrides becomes too confusing/impractical, you may use this tweaked version:

/**
 * Merge keys of U into T, overriding value types with those in U.
 */
type Override<T, U extends Partial<Record<keyof T, unknown>>> = FinalType<Omit<T, keyof U> & U>

FinalType is a type utility that improves the readability of the final resulting type prodived by your lsp (as shown in your editor). Instead of seeing a bunch of utility types wrapping your initial type, you get the final type as if you had written it yourself.

Here is a definition (taken from this answer):

/**
 * Make a type assembled from several types/utilities more readable.
 * (e.g. the type will be shown as the final resulting type instead of as a bunch of type utils wrapping the initial type).
 */
type FinalType<T> = T extends infer U ? { [K in keyof U]: U[K] } : never

So, reusing the original question's example:

type InitialType = {
    one: number;
    two: string;
    three: {
        four: string,
        five: number,
    }
}

You may use Override this way:

type Transformed = Override<InitialType, {
    three: {
        four: string
        five: number[]
    }
}>

I tested all this in this typescript playground

Romain Vincent
  • 2,875
  • 2
  • 22
  • 29
  • `type Override` works but without `ReadableType` type. Also it is not defined in answer. – Alex Po Apr 12 '23 at 13:48
  • 1
    I suggest the following improved version of Override: `type Override>> = Omit & U;` this replaces the Pick>, which I think just picks all attributes of T that are not in O, with Omit all attributes that are in O. This is a little bit shorter and does the same thing in the end I think. Also uses unknown instead of any. – Marnix Jun 12 '23 at 09:49
  • @AlexPo thanks, it seems I failed to double check after a last minute rename. Fixed that and renamed everything to FinalType, plus changed the docstring. – Romain Vincent Jun 28 '23 at 08:31
  • @Marnix neat suggestion. I changed the definition in the rewritten answer. – Romain Vincent Jun 28 '23 at 08:56
2

I was using the same method previously by doing an Omit first and then merging with &, so I made a utility type Replace:

type Replace<
  TypeToBeChecked,
  KeyToBeReplaced extends keyof TypeToBeChecked,
  NewValueToUse
> = Omit<TypeToBeChecked, KeyToBeReplaced> & {
  [P in KeyToBeReplaced]: NewValueToUse
}

We can use it like this:

type NewType = Replace<OldType, KeyInOldType, NewValueForTheSameKey>
Adnan Sheikh
  • 169
  • 1
  • 4
0

Here is an override(deep) version:

type Override<T, TypeToReplace, TypeToReplaceWith> = {
  [K in keyof T]: T[K] extends Record<string, unknown> ? Override<T[K], TypeToReplace, TypeToReplaceWith> : T[K] extends TypeToReplace ? TypeToReplaceWith : T[K]
}
nikksan
  • 3,341
  • 3
  • 22
  • 27