3

It seems that sometimes TypeScript (of recent version) fails to narrow union types even if type guard is present. Is this behavior a bug or a feature:

Preamble:

// some config
interface Config {
    name: string;
    option1?: number;
    option2?: boolean;
}

// arbitrary type
interface Entity {
    a: number;
    b: number;
}

// name aware type guard for entity property-to-config map
// some default config may be replaced with a property name
type TConfigSet<TData> = {
    [P in keyof TData]: (Config & { name: P }) | P;
}

// example of TConfigSet usage
const EntityConfigs: TConfigSet<Entity> = {
    a: {
        name: 'a',
        option2: true
    },
    b: 'b'
}

Question:

// this function compiles
function TypeLooseFieldToName(name: string | Config): string {
    if (typeof name === 'string') return name;
    else return name.name;
}

// this one doesn't
function TypeStrictFieldToName<TData>(name: keyof TData | { name: keyof TData }): keyof TData {
    if (typeof name === 'string') return name;
    else return name.name; // still keyof TData | { name: keyof TData }, should be shrinked to { name: keyof TData }
}

2 Answers 2

1

It seems to be a bug in the type checker, because the TypeScript handbook says "a keyof T type is considered a subtype of string."

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html

As a workaround, the type guard can be inverted to rule out the custom type first:

function hasName(obj: string | { name: string }): obj is { name: string } {
    return typeof obj.name === 'string';
}

function getName<TData>(name: keyof TData | { name: keyof TData }): keyof TData {
    if (hasName(name)) return name.name;
    else return name;
}

// compiles with valid keys
getName<Entity>('a');
getName<Entity>({ name: 'a' });

// doesn't compile with invalid keys
getName<Entity>('z');
getName<Entity>({ name: 'z' });

You could search the TypeScript Issues in GitHub and file a new issue if this hasn't been addressed previously:

https://github.com/Microsoft/TypeScript/issues

Sign up to request clarification or add additional context in comments.

Comments

0

typeof name === 'string' does not work as type guard for keyof TData because apparently string and keyof TData are assumed to be two different types by the compiler. If you add your own custom type guard for keyof TData it works:

function TypeStrictFieldToName<TData>(name: keyof TData | { name: keyof TData }): keyof TData {
    if (isKeyofTData<TData>(name)) return name;
    else return name.name; // type of name here is { name: keyof TData }
}

function isKeyofTData<TData>(name: keyof TData | {}): name is keyof TData {
    return typeof name === 'string';
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.