2

How do we prevent TypeScript from expanding a string literal type into a string?

E.g. in the Playground

type PropertyName = 'Property1' | 'Property2';

type ObjectWithPropertyName =
    { Property1: string; } |
    { Property2: string; };

const obj1: ObjectWithPropertyName = {
    ['Property1']: 'works',
}

const prop: PropertyName = 'Property1';
const obj2: ObjectWithPropertyName = {
    [prop]: 'works',
}

const func = (prop: PropertyName) => {
    const obj: ObjectWithPropertyName = {
        [prop]: "fails",
    };
};

const funcToo = (prop: PropertyName) => {
    const propInner: PropertyName = prop;
    const obj: ObjectWithPropertyName = {
        [propInner]: "fails",
    };
};

In the last case the error is this:

Property 'Property1' is missing in type '{ [x: string]: string; }' but required in type 'ObjectWithPropertyName'.

3
  • but ObjectWithPropertyName can have only Property1 not Property2 as prop could be.. Even if you fix this, the type will still be inferred to { [x: string]: string; }. I don't think you will get away without a type assertion here.. Commented Dec 4, 2018 at 23:37
  • @TitianCernicova-Dragomir Nice catch. I updated the question to address that concern. Commented Dec 4, 2018 at 23:39
  • @TitianCernicova-Dragomir I haven't even been able to make it work with a type assertion. Commented Dec 4, 2018 at 23:40

2 Answers 2

2

The reason it works for the const example is that there typescript knows prop can only be 'Property1'. When you deal with parameters that can have any of a number of value, so typescript will infer a type with an index signature.

For example, while not particularly useful, this also works, since prop can only be Property1:

const func = (prop: 'Property1') => {
    const obj: ObjectWithPropertyName = {
        [prop]: "fails",
    } ;
};

The simplest solution is to use a type assertion to force the type you want for obj:

const func = (prop: PropertyName) => {
    const obj = {
        [prop]: "fails",
    } as ObjectWithPropertyName;
};

Depending on what you are trying to do, you might consider a different approach, for example we can capture the actual constant passed in an return a Record that contains whatever that key is:

const func = <T extends PropertyName>(prop: T) => {
    const obj : Record<T, string> = {
        [prop]: "ok",
    };
    return obj;
};
func('Property1').Property1
Sign up to request clarification or add additional context in comments.

Comments

2

It's not passing through a function that causes this.

When it 'works', it works because somehow the compiler is still narrowing the type of prop to Property1

const prop: PropertyName = 'Property1';
const obj2: ObjectWithPropertyName = {
    [prop]: 'works',
}

because if you add type assertion 'Property1' as PropertyName it starts failing with the same error

const prop: PropertyName = 'Property1' as PropertyName;
const obj2: ObjectWithPropertyName = {
    [prop]: 'works',
}

Essentially, when prop has union type 'Property1' | 'Property2', the value {[prop]: 'something'} is not assignable to {Property1: string}, and it's not assignable to {Property2: string} either, making it nonassignable to the union type.

You can make it work by doing exhaustive check like this:

const func = (prop: PropertyName) => {
    const obj: ObjectWithPropertyName = 
        prop === 'Property1' ? { [prop]: "works" } : { [prop]: "works" }
};

2 Comments

Yeah the exhaustive check version is the typesafe solution but it gets really painful really fast :(
Yes, someone really ought to teach the compiler how to do case analysis

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.