Typescript: [Bug] [keyof] Unexpected behavior of `keyof` of an interface with optional properties

Created on 5 Jun 2018  路  4Comments  路  Source: microsoft/TypeScript


TypeScript Version: 2.9.1


Search Terms:
keyof,optional
Code

export type Map<T = {}> = {
  readonly [P in Extract<keyof T, string>]: A<P>;
};

export interface A<TNameType extends string = string> {
    test: string;
}

export interface B {
    required: string;
    optional?: string;
}

export const Test: Map<B> = {
    required: 'required',
}

Expected behavior:

No errors should be reported, because an optional property should not be required as a result of keyof.

Actual behavior:

Type '{ required: string; }' is not assignable to type 'Map<B>'.
  Property 'optional' is missing in type '{ required: string; }'.

It was working in 2.8.3, without Extract<>

Playground Link:
Playground Link

Related Issues:

Design Limitation

Most helpful comment

I think if you use a layer of indirection you can get the desired homomorphicity back (homomorphicness? homomorphologicality?). Specifically, make a new type parameter K extends keyof T, assign Extract<keyof T, string> to it, and then map over that. Like this:

export type Map<T = {}, K extends keyof T = Extract<keyof T, string>> = {
  readonly [P in K]: A<P>;
};

or, equivalently

type OnlyStringKeyProps<T> = Pick<T, Extract<keyof T, string>>; 
export type Map<T = {}> = {
  readonly [P in keyof OnlyStringKeyProps<T>]: A<P>;
};

Then the following should work (note that the type of the required property had to be modified to type {test: string} as defined in A<> ; I assume OP wasn't meaning to make this error):

export const Test: Map<B> = {
  required: { test: 'required' },
};  // no error now

So at least there's a workaround? Not sure if this is the way to do it going forward, or if there are other ideas.

All 4 comments

This is pretty unfortunate - our workaround to only include string keys was to use Extract<keyof T, string>, but mapped types that use that trick are no longer considered homomorphic mapped types, which means that we can't preserve modifiers on properties.

Is there a reason you only want to include string keys?

I think if you use a layer of indirection you can get the desired homomorphicity back (homomorphicness? homomorphologicality?). Specifically, make a new type parameter K extends keyof T, assign Extract<keyof T, string> to it, and then map over that. Like this:

export type Map<T = {}, K extends keyof T = Extract<keyof T, string>> = {
  readonly [P in K]: A<P>;
};

or, equivalently

type OnlyStringKeyProps<T> = Pick<T, Extract<keyof T, string>>; 
export type Map<T = {}> = {
  readonly [P in keyof OnlyStringKeyProps<T>]: A<P>;
};

Then the following should work (note that the type of the required property had to be modified to type {test: string} as defined in A<> ; I assume OP wasn't meaning to make this error):

export const Test: Map<B> = {
  required: { test: 'required' },
};  // no error now

So at least there's a workaround? Not sure if this is the way to do it going forward, or if there are other ideas.

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments

blendsdk picture blendsdk  路  3Comments

wmaurer picture wmaurer  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments