34

Using the new conditional types in TypeScript (or maybe another technique), is there a way to pick only certain properties from an interface based on their modifiers? For example, having...

interface I1 {
    readonly n: number
    s: string
}

I would like to create a new type based on the previous one which looks like this:

interface I2 {
    s: string
}
DanielM
  • 1,106
  • 3
  • 17
  • 27

2 Answers2

76

Update 2018-10: @MattMcCutchen has figured out that it is possible to detect readonly fields (invalidating the struck-out passage below), as shown in this answer. Here is a way to build it:

type IfEquals<X, Y, A=X, B=never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

type WritableKeys<T> = {
  [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
}[keyof T];

type ReadonlyKeys<T> = {
  [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, never, P>
}[keyof T];

If you want to extract the writable fields from an interface, you can use the above WritableKeys definition and Pick together:

interface I1 {
    readonly n: number
    s: string
}

type I2 = Pick<I1, WritableKeys<I1>>; 
// equivalent to { s: string; }

Hooray!

For `readonly`, I don't think you can extract those. I've [looked at this issue before](https://github.com/Microsoft/TypeScript/issues/13257#issuecomment-308528175) and it wasn't possible then; and I don't think anything has changed.

Since the compiler doesn't soundly check readonly properties, you can always assign a {readonly n: number} to a {n: number} and vice-versa. And therefore the obvious TSv2.8 conditional type check doesn't work. If, for example, {n: number} were not considered assignable to {readonly n: number} then you could do something like:

// does not work, do not try this
type ExcludeReadonlyProps<T> = Pick<T,
  { [K in keyof T]-?:
    ({ readonly [P in K]: T[K] } extends { [P in K]: T[K] } ? never : K)
  }[keyof T]>

type I2 = ExcludeReadonlyProps<I1> // should be {s: string} but is {} 

But you can't. There's some interesting discussion about this in a GitHub issue originally named "readonly modifiers are a joke".

Sorry! Good luck.


For optional properties, you can indeed detect them and therefore extract or exclude them. The insight here is that {} extends {a?: string}, but {} does not extend {a: string} or even {a: string | undefined}. Here's how you could build a way to remove optional properties from a type:

type RequiredKeys<T> = { [K in keyof T]-?:
  ({} extends { [P in K]: T[K] } ? never : K)
}[keyof T]

type OptionalKeys<T> = { [K in keyof T]-?:
  ({} extends { [P in K]: T[K] } ? K : never)
}[keyof T]

type ExcludeOptionalProps<T> = Pick<T, RequiredKeys<T>>

type I3 = { 
  a: string, 
  b?: number, 
  c: boolean | undefined
}

type I4 = ExcludeOptionalProps<I3>;
// {a: string; c: boolean | undefined} 

So that's good.


Finally, I don't know if you want to be able to do stuff with the class-only property modifiers like public, private, protected, and abstract, but I would doubt it. It happens that the private and protected class properties can be excluded pretty easily, since they are not present in keyof:

class Foo {
  public a = ""
  protected b = 2
  private c = false
}
type PublicOnly<T> = Pick<T, keyof T>; // seems like a no-op but it works
type PublicFoo = PublicOnly<Foo>; // {a: string} 

But extracting the private or protected properties might be impossible, for the same reason that excluding them is so easy: keyof Foo doesn't have them. And for all of these including abstract, you can't add them to properties in type aliases (they are class-only modifiers), so there's not much I can think of to do to touch them.

starball
  • 20,030
  • 7
  • 43
  • 238
jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Yup I was looking at this as well, don't think there is a way to create any sort of incompatibility based on readonly – Titian Cernicova-Dragomir Mar 30 '18 at 19:03
  • 1
    Very interesting, thanks a lot. Two little more questions come to my mind now: 1) do you think your type will work if something like `--enforceReadonlyAssignability` gets added to the compiler? and 2) what about other modifiers, e.g. the optional modifier `?`? – DanielM Mar 30 '18 at 21:12
  • Updated with info about optional and other modifiers. As for whether my type would work with a hypothetical `readonly` strictness flag... I hope so? There's really no way to know without trying, since it all depends on how the strictness feature is implemented. – jcalz Mar 31 '18 at 00:35
  • Thank you for the time taken, your reply was very helpful! – DanielM Apr 01 '18 at 10:47
  • 3
    In case anyone finds this thread in the future, there _is_ [a way to check for readonly fields](https://stackoverflow.com/a/52473108) by abusing the assignability rule for conditional types. (I guess in theory I'm supposed to post an answer here since this question is the most popular and then close the other question as duplicate? I can't be bothered.) – Matt McCutchen Oct 23 '18 at 12:54
  • 1
    @MattMcCutchen Wow, great! I will edit this answer. – jcalz Oct 23 '18 at 13:00
  • In the places where you write `[P in keyof T]-?:`, I think it can just as well be replaced by `[P in keyof T]:` because the type you build is temporary and only the value types are pulled out through `[keyof T]` when you do `{[P in keyof T]: /* filter here */}[keyof T]`. For better clarity, I will go ahead and make the edit. – spenceryue Jan 23 '19 at 08:32
  • 1
    @spenceryue I see the edit has already been rejected by others but I'd like to explain why the `-?` is necessary: in `--strictNullChecks` mode optional properties get `| undefined` automatically appended to their value types. And since the filter is a [homomorphic mapped type](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types), the default behavior is to transform optional properties to optional properties. Assuming you want the filter to produce something like `"foo" | "bar"` and not `"foo" | "bar" | undefined`, you need to explicitly remove the optional modifier. – jcalz Jan 23 '19 at 14:41
  • I see. Thank you for explaining. I realize now I had been comparing the two approaches in TypeScript playground without `strictNullChecks` enabled. – spenceryue Jan 23 '19 at 19:27
  • I found that extracting optionals when there are none causes a problem: https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgZgUQI4B4AaAaKBNLBBAXkygCECA7CANwgCcA+KAgKCigAoUAVe9gSiaMuUCAA9gEcgBMAzlDRQA-FACMUAFxQATALETpM1h268BBISPGTZOJao3aByvA5IBuZs1CQoAeTDAANIyJkxQAN5QANqBUACW5FAA1hAgAPZwUFwAugC0iupG7OEAvpb6NpFRAArxiYHZmlwx2VBlyrGalDS0fMwlUSnpmTme3tD+wHFp5ACGADYhPGEAShBIAK5xtBBSKNVxAMZJ3FiTwSb09J4JErRws4fQALJxouG06rPkIG5QaQVviASsxDjMZMB-gEZOpJtM5osUK9RIwCBEjGw0uoVBgMVBPlooAB6ImWSCHCRSKBggC2YHms1u-VB4LS8wgADp5mkAObsNLQvrMW50B5PKAAOTSkxkHy+PxBYPIEKg5DSAuAMLhMwWISlMtR6LYbE+AFZiaS1dS0nSGbcAIT9IA – Mircode May 25 '20 at 08:28
  • For anyone that needs a one line solution, see here: https://www.typescriptlang.org/play?#code/C4TwDgpgBA6gTgS2AQwEYBsIB4AqA+KAXigAUEBjAa1wBooBvAKCigG0SoEA7KSiEAPYAzKDgC6AWgD8ALigAKLAFU88gJRECSqBAAewCFwAmAZwZsAipx4kxcnOzFQAvlClQAjFDkAmDXoNjE2YFZVUNQi0dfUNTcwk4CGQjAS50EEtrUjtRRxc3T28oPwKOOS4IADcIOABuRmdWPkERcTx6xm4DOCFkcmgASS8mFhZE5NT0qC5ygFcAW1QakJYTORNgRC4AcwbGRlBIKAGfIlhEFAxsIfaoIA – sportzpikachu Dec 29 '20 at 04:55
  • Is it possible for this to be done recursively -- e.g. *ALL* readonly fields are removed, including from sub-interfaces? Maybe this needs to be a separate question. – slifty Sep 12 '22 at 19:29
1

I've refactored the types in the accepted solution and some may prefer my types as they are simpler:

/**
 * Returns the required keys of an object
 * @example
 * type U = RequiredKeys<{ a?: 'a'; b: 'b'; c: 'a' }> // "b" | "c"
 */
export type RequiredKeys<T extends object> = keyof {
  [K in keyof T as T extends Record<K, T[K]> ? K : never]: K
}
/**
 * Returns the optional keys of an object
 * @example
 * type U = OptionalPropertyOf<{ a?: 'a'; b: 'b', c?: 'a' }> // "a" | "c"
 */
export type OptionalKeys<T extends object> = keyof {
  [K in keyof T as T extends Record<K, T[K]> ? never : K]: K
}
TrevTheDev
  • 2,616
  • 2
  • 18
  • 36