Skip to content

Mapped type used with keyof not narrowing #36050

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
whitneyit opened this issue Jan 7, 2020 · 4 comments
Closed

Mapped type used with keyof not narrowing #36050

whitneyit opened this issue Jan 7, 2020 · 4 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@whitneyit
Copy link
Contributor

TypeScript Version: Nightly

Search Terms:
computed type
mapped type narrow
keyof narrow type
type index narrow

Expected behavior:
No errors.

Actual behavior:
The type for value is not being narrowed on either side of the if statement.

Related Issues:

Code

interface Example {
    b: boolean;
    n: null;
}

function onlyBoolean(arg: boolean): void {
    // do nothing
}

function onlyNull(arg: null): void {
    // do nothing
}

function test<K extends keyof Example>(value: Example[K]): void {
    if (value === null) {
        // Argument of type 'Dict[K]' is not assignable to
        // parameter of type 'null'.
        onlyNull(value);
        return;
    }
        // Argument of type 'Dict[K]' is not assignable to
        // parameter of type 'boolean'.
    onlyBoolean(value);
}
Output
"use strict";
function onlyBoolean(arg) {
    // do nothing
}
function onlyNull(arg) {
    // do nothing
}
function test(value) {
    if (value === null) {
        // Argument of type 'Dict[K]' is not assignable to
        // parameter of type 'null'.
        onlyNull(value);
        return;
    }
    // Argument of type 'Dict[K]' is not assignable to
    // parameter of type 'boolean'.
    onlyBoolean(value);
}
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

@Cryrivers
Copy link

Cryrivers commented Jan 7, 2020

I'm not sure if it is relevant. TypeScript 3.7.4 (also Nightly Build) infers my variable as { [eventName in Keys]?: States | undefined; }[Keys] instead of further reducing it to States | undefined.

Playground Link

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jan 7, 2020
@RyanCavanaugh
Copy link
Member

FYI this isn't a mapped type.

The function could be written as

function test(value: Example[keyof Example]): void {
    if (value === null) {
        onlyNull(value);
        return;
    }
    onlyBoolean(value);
}

without loss of generality; if you have a more complete example that requires K I could advise further.

The narrowing here can't apply because value isn't a union type -- it's a deferred lookup type over a generic type parameter. #33014 would likely be a prerequisite to handling this correctly.

@whitneyit
Copy link
Contributor Author

Consider this more complex example:

const I18N_EXAMPLE_WITHOUT_DATA_KEY = `Example.WithoutData`;
const I18N_EXAMPLE_WITH_DATA_KEY = `Example.WithData`

type I18nKey = typeof I18N_EXAMPLE_WITHOUT_DATA_KEY | typeof I18N_EXAMPLE_WITH_DATA_KEY;

interface I18nDataMap {
    [I18N_EXAMPLE_WITHOUT_DATA_KEY]: null,
    [I18N_EXAMPLE_WITH_DATA_KEY]: { count: number },
}

function onlyNull(arg: null): void {
    // do nothing
}

function interpolate<K extends I18nKey>(key: K, data: I18nDataMap[K]): string {
    const value = `Use ${key} to fetch value from somewhere`;
    if (data === null) {
        // Argument of type 'I18nDataMap[K]' is not assignable to
        // parameter of type 'null'.
        onlyNull(data);
        return value;
    }
    const result = `Some interpolated value`;
    return result;
}

In the above example, K extends I18nKey is used to ensure the relationship between key and data is preserved. Unfortunately my initial example did not show that relationship.

@pleunv
Copy link

pleunv commented Feb 24, 2021

Apologies for digging this up, but I think I'm running into a similar (or the same) issue. Given this code (playground link):

function exhaustiveCheck(check: never): never {
  throw new Error('ERROR!');
}

interface Settings {
  settingA: boolean;
  settingB: string;
  settingC: number;
}

function getValue<T extends keyof Settings>(key: T): Settings[T] {
  if (key === 'settingA') {
    return true;
  } else if (key === 'settingB') {
    return 'test';
  } else if (key === 'settingC') {
    return 5;
  } else if (key === '') {
    exhaustiveCheck(key);
  }
}

I would expect both the key to be narrowed, and the return value to be mapped to the corresponding mapped type. However, neither seems to work:

image

Am I completely missing something here, or is this simply not possible? I have similar approaches in other places that do seem to work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants