2

I have a Typescript class like this:

    export class Contract {
        constructor (
           public id: string,
           public name: string,
           public spend: number
        ) {}
    }

This is loaded by a service using an intermediate class like this:

    export class ContractService {
        // the stuff you would expect

        public loadContracts() {
            this.httpService.get(this.contractEndpoint).subscribe((result) => this.createContracts(result));
        }

        private createContracts(contracts: Array<Contract>) {
            for ( let contract of contracts ) {
                console.log("Contract spend is "+contract.spend+": "+( typeof contract.spend));
            }
        }
    }

When I run it, in my console I see this:

 Contract spend is 10000: string 
 Contract spend is 1222: string
 Contract spend is 20001: string

However if I try to use parseInt(contract.spend) then the Typescript compiler refuses because it knows contract.spend to be a number, so at compilation it is aware what the value should be.

I assume that what is happening is that the JSON from my rest service is returning the spend field as a quoted value, but it seems to be subverting one of the core advantages of Typescript in a way that fails silently. What do I need to do to ensure that either my numeric field contains a number or my code fails when the wrong type is handed to it?

3
  • 1
    Is your endpoint delivering the data as string (" with quotes) or as integer? There is no conversion inside TypeScript Commented Aug 2, 2019 at 9:41
  • You can convert string to number by let stringToNumber: number = +contract.spend. Commented Aug 2, 2019 at 9:49
  • @hrdkisback It also works with Number(contract.spend) but I was interested in why it failed and yet didn't acknowledge that there was a problem. Commented Aug 2, 2019 at 9:53

3 Answers 3

2

TypeScript works at compile time, but it compiles to JavaScript. So at runtime, you are left with good old JS.

That means: If your service, unbeknownst to TypeScript, delivers string values for contract.spend, TypeScript has no way of knowing it.

If you want to take advantage of static typing in that case, let TypeScript know which type the response body of your HTTP call has. Then actively convert the response from the response body type to your expected type.

For example:

type HttpContractResponse = {
    spend: string
}[];

export class ContractService {
    // the stuff you would expect

    constructor(private httpService: HttpService,
                private contractEndpoint = 'http://endpoint.com') {}

    public loadContracts() {
        this.httpService.get<HttpContractResponse>(this.contractEndpoint)
            .subscribe((result) => this.createContracts(result));
    }

    private createContracts(rawContracts: HttpContractResponse) {
        const contracts: Contract[] = rawContracts.map(rc => {return {
            ...rc,
            spend: +rc.spend
        }});
        for (let contract of contracts) {
            console.log('Contract spend is ' + contract.spend + ': ' + (typeof contract.spend));
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

This is a good answer, probably the right way to do it, thank you.
1

I suggest you look at typescript-json-validator, it will provide typesafe validation that some unknown json matches your typescript interface (you should define interfaces rather than classes for JSON data, it can't contain methods so an interface is sufficient to describe it).

File app/interfaces/contract.ts:

export interface IContract {
    id: string;
    name: string;
    spend: number;
}

Run this command (after installing the module) it will produce a new file contract.validator.js:

yarn typescript-json-validator --collection --aliasRefs --noExtraProps --strictNullChecks --coerceTypes app/interfaces/contract.ts

Here's an example of use:

import {validate} from "./interfaces/contract.validator";

const validateContract = validate('IContract');

const data: unknown = JSON.parse('{ "id": "42", "name": "Joe", "spend": "10000"}');
const contract = validateContract(data);
console.log(`Contract spend is ${contract.spend}: ${typeof contract.spend}` );

Output is:

Contract spend is 10000: number

The type of contract is IContract here and all the types will match. You don't have to tell typescript const contract: IContract as it will infer the type correctly but you can do that if you prefer.

If the JSON doesn't contain the correct fields, or they don't have the expected types it will throw an error. The --coerceTypes option in the command allows some conversions e.g. string to number. You can also include additional constraints such as regex patterns in comments in the interface, see the documentation. If you put multiple interfaces in one file the --collection option ensures they are all available just create a separate validator passing the name of each interface.

There are some annoying restrictions so stick to simple strings and numbers in the interface. e.g. don't use Date as a type as it will validate the field is a string containing an ISO formatted date but it won't coerce the type so you still end up with a string. You can however use comments to say it is a string with the correct date or date-time format and then construct a class with the correct fields from the validated interface.

Comments

0

TypeScript types are removed during compilation - since JavaScript (which TypeScript is transpiled to) does not have a concept of static types. Hence there is no way to ensure the value to be of the correct type during runtime "out-of-the-box".

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.