Typescript - Why can't this string literal type be inferred?
Asked Answered
J

3

7

The following snippet does not pass the type check:

type TaskType = 'SIMPLE' | 'COMPLEX'

interface TaskDefinition {
    name: string,
    task: string,
    taskType: TaskType
};

const test: TaskDefinition = { 
    name: '',
    task: '',
    taskType: 'SIMPLE' // This is fine
};

const tasks : TaskDefinition[] = ["apples", "pears"].map(i => {
    return {
        name: i,
        task: i,
        taskType: 'SIMPLE' // This one is not
    };
})

{ name: string; task: string; taskType: string; }[] is not assignable to type TaskDefinition[].

Try it

It seems that taskType gets inferred as string instead of TaskType despite the target type being TaskDefinition

What's causing this and how can I fix it?

Janetjaneta answered 8/6, 2018 at 14:25 Comment(0)
M
18

Typescript will infer string literal types only when you assign it to a const. When you are creating object literals, the compiler will infer string for string constants not string literal types. If you assign the object literal directly to something that requires a string literal type, that is ok, as in this case the compiler just checks that the string constant is assignable to the string literal type.

The simple solution here is to specify the type argument to map, this will still preserve compiler checks on the return value from map :

const tasks = ["apples", "pears"].map<TaskDefinition>(i => {
    return {
        name: i,
        task: i,
        taskType: 'SIMPLE'
    };
})

Or to use a type assertion on the string to the expected string literal type:

const tasks:TaskDefinition[] = ["apples", "pears"].map(i => {
    return {
        name: i,
        task: i,
        taskType: 'SIMPLE' as 'SIMPLE'
    };
}) 

Edit Since typescript 3.4 (PR) you can also use an as const assertion to get the string literal type:

const tasks:TaskDefinition[] = ["apples", "pears"].map(i => {
    return {
        name: i,
        task: i,
        taskType: 'SIMPLE' as const
    };
}) 

End Edit

You could also type assert directly on the return value, but this will disable some checks on the return value:

const tasks:TaskDefinition[] = ["apples", "pears"].map(i => {
    return <TaskDefinition>{
        wrongValue: "", // no error since we are asserting
        name: i,
        task: i,
        taskType: 'SIMPLE'
    };
}) 
Milton answered 8/6, 2018 at 14:31 Comment(2)
I agree that .map<TaskDefinition> is probably the most elegant solution for this.Intussuscept
@Intussuscept 10x, I also added a reason why specifying the type parameter on map is preferable to a straight up type assertionMilton
I
3

Reason

The compiler isn't narrowing 'SIMPLE' from string to TaskType, so you'll need to help it with a type assertion. There are two options for that.

Type Assertion at Object

const tasks: TaskDefinition[] = ["apples", "pears"].map(i => {
    return <TaskDefinition> {
        name: i,
        task: i,
        taskType: 'SIMPLE' // This one is not
    };
});

Type Assertion at Value

const tasks: TaskDefinition[] = ["apples", "pears"].map(i => {
    return {
        name: i,
        task: i,
        taskType: <TaskType>'SIMPLE' // This one is not
    };
});
Intussuscept answered 8/6, 2018 at 14:31 Comment(0)
A
2

I see three ways of fixing this:

return {
        name: i,
        task: i,
        taskType: 'SIMPLE'
    } as TaskDefinition;

Or:

const tasks: TaskDefinition[] = ["apples", "pears"].map(i => {
    return {
        name: i,
        task: i,
        taskType: 'SIMPLE'
    };
}) as TaskDefinition[];

Or:

const tasks: TaskDefinition[] = ["apples", "pears"].map(i => {
    return {
        name: i,
        task: i,
        taskType: 'SIMPLE' as TaskType
    };
});

As to why, I'm not completely sure. Why not use string enums in place of the custom string type?

Areopagite answered 8/6, 2018 at 14:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.