Fabio Iotti
Fabio Iotti

Reputation: 1540

TypeScript without casting

I've been coding TypeScript for a few years now. I've seen it evolve and improve so much with every release. Still, all this static typing perfection falls apart with casts.

Despite coding in TypeScript so much, I still don't understand how to avoid casting in functions that generate object transformations.

Like in the following example: I want to take an object as input, and return a transformed copy of this object as output.

function stringifyProperties<T>(obj: T): { [P in keyof T]: string } {
    let stringified: { [P in keyof T]: string } = {} as any;

    for (let k in obj)
        stringified[k] = JSON.stringify(obj[k]);

    return stringified;
}

That as any cast really breaks the spell, but I can't find a way to avoid it. Is it even possible?

Thanks!

Upvotes: 2

Views: 1332

Answers (3)

Tim Lundqvist
Tim Lundqvist

Reputation: 415

In TypeScript the real problem isn't casting by it self but specifically the any type which looses all type information and static checking. To avoid using any-casts for accumulators you may mark these as being a Partial application of your structure and when you're sure these are complete cast it to the full object without "any" intermediary typeless state.

In your case I would have used the following solution:

type StringifiedProps<T> = { [P in keyof T]: string; };
function stringifyProperties<T>(obj: T): StringifiedProps<T> {
    const stringified: Partial<StringifiedProps<T>> = {};

    for (const k of Object.keys(obj) as Array<keyof T>) {
        stringified[k] = JSON.stringify(obj[k]);
    }

    return stringified as StringifiedProps<T>;
}

Upvotes: 3

Jake Holzinger
Jake Holzinger

Reputation: 6063

The reason it falls apart is because you're initializing the variable with your special "stringify" type as an empty object. Casting the empty object to any merely suppresses the error.

Take for instance this simple interface:

interface Foo {
    bar: string;
    baz?: string;
}

In order to assign to a variable declared with type Foo the assignment must meet the requirements of the interface.

let foo: Foo;
foo = {bar:'fizz'}; // No error.
foo = {baz: 'buzz'}; // Error, bar must be defined.

Since your "stringify" type says that every property from T exists in the new object typescript complains because you're assigning an empty object to a type with required properties.

The only way to avoid casting would be to declare your "stringify" type as having optional properties for every property in T.

let stringified: { [P in keyof T]?: string } = {};

This means you would also need declare your return type the same way.

If you choose to keep the cast, you could instead perform the cast as you return, and declare the variable as any up front since you are building the object rather than declaring it all at once as would be required. This is more of a personal preference, but I think it's more readable and accurately describes what is really going on.

let stringified: any  = {};
...
return stringified as { [P in keyof T]: string };

Upvotes: 1

Fenton
Fenton

Reputation: 250932

I don't believe any spell has been broken. TypeScript is optionally typed, and you want to perform an operation within your function that is dynamic - so the any type allows that dynamic behaviour.

The three lines of code where you create an empty object with no properties (which cannot possibly satisfy the type you have supplied), and the subsequent loop where you add each of the properties to this new object _ought_to be dynamic statements, which the any type is there for.

The only way to avoid this would be to write a single line of code that could be resolved into the correct type.

I always like to make the code as honest as possible, so I would probably state explicitly that stringified requires dynamic behaviour.

function stringifyProperties<T>(obj: T): { [P in keyof T]: string } {
    let stringified: any = {}

    for (let k in obj)
        stringified[k] = JSON.stringify(obj[k]);

    return stringified;
}

That way, you only promise the correct type on the final line, not before (i.e. you don't say it is { [P in keyof T]: string } until it is { [P in keyof T]: string }, which is after you add all of the required properties.

The result to callers is the same:

const objA = {
    A: 1,
    B: 2,
    C: 3
}

//const x: { A: string; B: string; C: string; }
const x = stringifyProperties(objA);

Upvotes: 2

Related Questions