6

I was trying to better understand the type safety in Typescript and came across this scenario where I have this function:

function test(x: number){
    console.log(typeof x);
}

If I call this method this way - test('1') It throws compile time error but it works fine if I change it to this:

let y: any = '1';
test(y);
//works fine
//Outputs "string"

From what I understand is that declaring 'x' as number only works during compile time and that Typescript only enforces compile time typesafety not runtime. So, I wanted to know if I understand this correctly or missing anything and also, what all are the different ways to ensure runtime typesafety?

Sachin Parashar
  • 1,067
  • 2
  • 18
  • 28

5 Answers5

8

TypeScript is two merged languages:

  1. Type level language
  2. Value level language

The first is visible as all type annotations, it is a syntax which not exists in pure JavaScript. Every type annotation and reserved words like type, interface, enum, as, in are part of type level language. What TS is doing during compilation as first is checking correctness of grammar and syntax of type level language and correctness of annotations over value level language.

The second is value level language, it is fully correct JS syntax. It also has most of features from ECMAScript proposals stage 3.

The first part is fully removed (exception is Enum which has representation in the runtime), the second stays in the runtime.

Back to the question about safety, yes TypeScript ensure safety during writing the code. You define the contract, write transformations of the contract, and TS is checking correctness of the code with relation to contract annotations. It removes a whole bunch of errors like typos, or using null/undefined objects methods and properties, it also gives visible definitions of the data during the program flow.

But, it does not secure the runtime. All type annotations are only assumptions, if we define API to have such and such structure of the end point response, then TS will guarantee that code will follow this structure, but if in the runtime, the structure will be different, the program will naturally fail, as contract not equals the data.

Back to your example

function test(x: number){
    console.log(typeof x);
}

Defining function test as a function which takes number as an argument, you are saying nothing different then number will be passed to the function. So the above implementation is really a constant, as typeof x will always return number, because that exactly what annotations says.

// below is equal to your implementation, as number has one specific type
// caution is equal for TS, not for the runtime!
function test() {
  return console.log('number')
}

If the function is polymorphic in terms of the input, then input should be annotated as such. It can happen that you don't know what input you can get, then you can implement checking of the structure. The proper name for that is - type guard. Consider below example

function test(x: unknown) {
  if (typeof x === 'number') {
    return x * 2; // return number
  }
  if (typeof x === 'string') {
    return x + '- I am string'; // return number
  }

  if (typeof x === 'object') {
    return x; // return object;
  }

  return null; // for others return null
}

Now function test has inferenced output as union string | number | object | null. By using control flow and conditions, TS is able to understand what the function returns.

Every time your code deals with some polymorphic type, you can use type guards to specify exactly with what type you are working. The checking is done by the structure (because only structure exists in the runtime, type only annotates the structure during code writing), so you can check typeof, instanceof or if object has specific keys or values.

Very important thing to remember - Type is a label for some existing runtime structure. The label/type not exists during runtime, but structure does. That is why TS is able to understand type guards, as every type refers to some structure.

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
  • This is interesting and related to a 'q' I have in another flow. Maybe you could you help. https://softwareengineering.stackexchange.com/questions/432545/extending-typescript-while-being-futureproof . Typeguard in this case might be able to assist with pattern matching immensely. – Pogrindis Oct 08 '21 at 02:33
5

Runtime type safety has nothing to do with TypeScript. The TypeScript team is very clear about the goals of the language, and you can read the following line in the "Non-goals" section of the wiki :

Provide additional runtime functionality or libraries.

If you're looking for runtime type safety, you'll have to look elsewhere.

On top of that, you're actively disabling the type-checking you could want on your variable by stating it is of type any. You can read about that behavior on the handbook.

The runtime type-checking strategy is the same in TS than in JS, because at runtime, there just isn't any TS anymore, it's all JavaScript.

Kewin Dousse
  • 3,880
  • 2
  • 25
  • 46
1

When you say let y: any = '1' you basically turn off type checking the variable y - so you should pass it to a method that needs a number. If you don't add the any type it will throw an error at compile time. Basically you're right, the :type annotations are good for compile-time type checking. It might be interesting to check the typescript playground how TSC compiles your code.

Lajos Gallay
  • 1,169
  • 7
  • 16
1

TypeScript guarantees type checking while compile only, it's true.

JavaScript has dynamic typing, so you can check it yourself using typeof and if but nobody using typescript do it.

Also you can enable all strict options in tsconfig.json and ts will strengthen the rules of verification.

UPD:

You also can use tslint and tslint-eslint-rules packages for code verification includes type rules like no-any

svltmccc
  • 1,356
  • 9
  • 25
1

JavaScript has an assertion function console.assert(assertion, message?). I typically use it like that in my functions:

/**
 * Checks if the value is in range
 *
 * @param value
 * @param fromInclude lower inclusive limit
 * @param toExclude upper exclusive limit
 */
export function inRange(value: number, fromInclude: number, toExclude: number): boolean {
    console.assert(!isNaN(value));
    console.assert(!isNaN(fromInclude));
    console.assert(!isNaN(toExclude));
    console.assert(fromInclude < toExclude);

    return value >= fromInclude && value < toExclude;
}

This is great at letting developers know early if something went wrong. And you can configure your build pipeline to remove all console.assert calls in production build so it's lighter and does not show warnings in console to your prod users. You can do all the type checks in assertions because many types type conversion will not cause errors but it's good to know you're getting string '237' instead of number 237 someplace and fix this.

waterplea
  • 3,462
  • 5
  • 31
  • 47
  • 1
    I think if you use typescript you should not do assert isNaN, it's redundancy – svltmccc Dec 02 '19 at 09:49
  • First, this is runtime check, as mentioned in other answers — TypeScript has nothing to do with runtime. This function can be called with data from server or user input that was not properly converted. TypeScript helps you develop but it cannot catch everything. Second, isNaN is a proper check here because NaN is of type number, but this function doesn't make sense if any input is NaN — this is a signal something went wrong prior to this function being called. – waterplea Dec 02 '19 at 10:00
  • 1
    Well, first, if you have not validation of user input it's not typescript troubles and if your backend returned string instead of number it's not typescript trouble too. You must validate user input once right after user has finished entering and you should not do it every time. Second, you must check for NaN right after you called function that can return NaN and you should not do it every time too. – svltmccc Dec 02 '19 at 10:25